Skip to content

Is there a way to get attributes on a union case? #2026

Open

Description

@anchann

Description

I'm generating UI forms from arbitrary types using reflection. One of the requirements I have is that union cases can be whitelisted to be shown as an option in the UI. For example, for a type like this:

type Action =
| [<UiVisible>] DoThis
| [<UiVisible>] DoThat
|               DoSomethingSecret

I want to only show DoThis and DoThat in the dropdown of choices, but not DoSomethingSecret.

I'm not that familiar with F# reflection APIs, but in my form generation code, I have a UnionCaseInfo, and that has a GetCustomAttributes: Type -> obj[] method which seemed like what I needed. Regretfully when trying to use it, I get

error FABLE: Cannot resolve Microsoft.FSharp.Reflection.UnionCaseInfo.GetCustomAttributes

from the Fable compiler.

I imagine it's the same as with other missing reflection functions — not implemented because it was either not yet needed by anyone, or because the only possible implementation is too heavy to and will bloat the library.

If the former, I'd like to try implementing it (guidance would be much appreciated).

If the latter, could you help me come up with some workaround? I think the best possible workaround would be to somehow make the heavier reflection APIs pluggable for those who need them, though I imagine this is a complex architectural change, since it's not only a matter of making the function available, but also a matter of plumbing all the data into the runtime representation of the objects.

Or maybe there's some other way of getting the attributes?

Code

open System
open FSharp.Reflection

type UiVisibleAttribute() = inherit System.Attribute()

type Action =
| [<UiVisible>] DoThis
| [<UiVisible>] DoThat
|               DoSomethingSecret

let theType: Type = typeof<Action>

let uiVisibleCases: List<string> =
    match theType with
    | t when FSharpType.IsUnion t ->
        FSharpType.GetUnionCases t
        |> Seq.choose (fun unionCase ->
            match unionCase.GetCustomAttributes typeof<UiVisibleAttribute> |> Array.isEmpty with
            | true  -> None
            | false -> Some unionCase.Name
        )
        |> Seq.toList
    | _ -> []

printf "%O" uiVisibleCases

Activity

  1. MangelMaxime commented on Apr 15, 2020

    @MangelMaxime
    Member

    The first problem here is that Fable doesn't emit Attribute information into the generated JavaScript.

    As far, as I remember Alfonso always said no to add Attribute information into the generated JavaScript file.

    In your case, you could just manually provide a list of the element you want to add in your view.

    type Action =
        | DoThis
        | DoThat
        | DoSomethingSecret
    
        static member UIAllowed =
            [
                DoThis
                DoThat
            ]

    You can also use an interface as a contract if you need something extensible:

    type UIAllowed<'T> =
        abstract member Whitelist : 'T list
    
    type Action =
        | DoThis
        | DoThat
        | DoSomethingSecret
    
        interface UIAllowed<Action> with
            member this.Whitelist =
                [
                    DoThis
                    DoThat
                ]
  2. anchann commented on Apr 15, 2020

    @anchann
    ContributorAuthor

    As far, as I remember Alfonso always said no to add Attribute information into the generated JavaScript file.

    Can we find out the rationale behind this, and document it in some kind of design-decisions.md doc, and then add a check design-decisions.md before making feature requests to see if your feature is not implemented for a good reason to the github issue template? This would save us time in the future, would be good for knowledge sharing and onboarding new contributors, and could potentially unblock some features if the original rationales no longer apply.

    In your case, you could just manually provide a list of the element you want to add in your view

    Thank you for thinking about ideas for my situation. This particular way of doing things won't scale well to union cases that have nontrivial fields:

    // imagine this is some complex type that you can't
    // cleanly make up a "stub" value for
    type SomeValue = SomeValue of string
    
    type Action =
    | DoThis
    | DoThat of SomeValue
    | DoSomethingSecret

    I thought maybe I could hack it by making a list of "case constructors", but can't find a type that works for both fieldful and fieldless cases:

    type Action =
    | DoThis
    | DoThat of SomeValue
    | DoSomethingSecret
    
    let uiVisibleCases: List<_ -> Action> = [
        DoThat
        DoThis // this line errors
    ]

    In the worst case I can make it a runtime-runtime (as opposed to runtime-initialization) concern and just do this:

    type Action =
    | DoThis
    | DoThat of SomeValue
    | DoSomethingSecret
    with
        static member IsUiVisible (action: Action) : bool =
            match action with
            | DoThis
            | DoThat _ -> true
            | _ -> false

    Anyway, my hope is that we discover that the original reason for attributes not being plumbed through is not a blocking one, and I can implement it. I'm really happy to finally be in a position where I can contribute something — it's much more realistic for me to contribute a feature that I actively need, than make time for contributions unrelated to direct work need.

  3. Zaid-Ajaj commented on Apr 15, 2020

    @Zaid-Ajaj
    Member

    Anyway, my hope is that we discover that the original reason for attributes not being plumbed through is not a blocking one, and I can implement it. I'm really happy to finally be in a position where I can contribute something

    @anchann Thanks a lot for being open to support this project!

    Can we find out the rationale behind this

    The rationale is AFAIK goes as follows: if you can do without Reflection then don't implement it because usually implementation requires quite the effort and use cases are limited but if someone want to step and implement it then it is more than welcome 😄

  4. anchann commented on Apr 16, 2020

    @anchann
    ContributorAuthor

    In the worst case I can make it a runtime-runtime (as opposed to runtime-initialization) concern and just do this

    I must have been too tired to think straight when I wrote this yesterday. It makes no sense. The dropdown to display the choice of union case is presented to the user before any concrete instances are constructed, so my IsUiVisible function that takes a concrete instance does not apply to this problem at all.

  5. anchann commented on Apr 16, 2020

    @anchann
    ContributorAuthor

    The rationale is AFAIK goes as follows: if you can do without Reflection then don't implement it because usually implementation requires quite the effort and use cases are limited but if someone want to step and implement it then it is more than welcome

    @Zaid-Ajaj Thanks for your reply. Makes sense. I'll keep trying to find a workaround, but if I can't find a satisfactory solution, will take a crack at implementing this.

  6. anchann commented on Apr 16, 2020

    @anchann
    ContributorAuthor

    Okay, I started working on implementing this, and I need some help. Please verify my thinking, and if it's correct, help me with the concrete details.

    Also, if there's helpful documentation on the details of Fable internals somewhere, that'd probably really help.

    Replacements.fs essentially just routes [reflection-related for our purposes here] calls to a typescript file Reflection.ts, which is the JS runtime implementation of these calls. In order to respond to these queries, the functions in Reflections.ts need access to reflection data. This data is provided to the JS runtime as part of the Fable compiler output. Presumably as the fable compiler walks the AST, it converts F# code to JS code, and as part of that it also generates reflection information that Reflection.ts gets to work with.

    The transformUnionReflectionInfo method in the Fable2Babel.fs file is responsible for generating the reflection data for union types. Until now, it was sufficient for each union case to output the name of the case and the fields (name, type) pairs. Now to this we need to add whatever information is necessary to serve the GetCustomAttributes request. (I'm taking it one step at a time, only dealing with attributes on union cases, and later will deal with other attributable entities)

    GetCustomAttributes takes an attribute type, and returns an array of objects — concrete instances of the attribute type.

    So we need to create these instances.

    At the Fable2Babel.fs level, we're dealing with the code at the lexical level, so we don't have "concrete instances" of the attribute, instead we have objects that represent the construction calls — name of attribute types and the list of arguments to this type's constructor.

    So, it seems that for each attribute, we need to output

    {
        typeName: string
        arguments: Array<{
            typeName: string
            value:    ???
        }>
    }
    

    Armed with this, inside Reflection.ts we can then (lazily if desired for performance) actually construct concrete instances of the attributes. But to do that I would need to be able to map a typeName: string to an actual instance of the type (or, rather, its constructor), which we know Fable exported somewhere into the runtime. Concretely, we need something that will, within the scope of Reflection.ts, dynamically, give us access to this:

    import { TestClass$$$$002Ector$$Z721C83C5 as TestClass$0024$0024$0024$0024002Ector$0024$0024Z721C83C5 } from "../../../../../LibAutoUi/src/Types.fs";
    

    when we ask it for LibAutoUi.Types.TestClass.

    What is this function?

    Once we know how to get our hands on the constructor function at runtime, we need to call it, with some values. This is something of a black box at the moment, because the FSharpAttribute type's ConstructorArguments type is IList<FSharpType * obj>. I've no idea what the obj actually is, i.e. what sort of representation of a value do we get, in what format to pass it over to the JS side, and then on JS side, how to turn it into an actual value.

    Help! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions