You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Is there a way to get attributes on a union case? #2026
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:
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
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
openSystemopenFSharp.ReflectiontypeUiVisibleAttribute()=inherit System.Attribute()typeAction=|[<UiVisible>] DoThis
|[<UiVisible>] DoThat
| DoSomethingSecret
lettheType:Type = typeof<Action>letuiVisibleCases: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
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 fortypeSomeValue= SomeValue ofstringtypeAction=| DoThis
| DoThat ofSomeValue| 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:
typeAction=| DoThis
| DoThat ofSomeValue| DoSomethingSecret
letuiVisibleCases: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:
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.
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 😄
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.
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.
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
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.
Activity
MangelMaxime commentedon Apr 15, 2020
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.
You can also use an interface as a contract if you need something extensible:
anchann commentedon Apr 15, 2020
Can we find out the rationale behind this, and document it in some kind of
design-decisions.mddoc, and then add acheck design-decisions.md before making feature requests to see if your feature is not implemented for a good reasonto 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.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:
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:
In the worst case I can make it a runtime-runtime (as opposed to runtime-initialization) concern and just do this:
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.
Zaid-Ajaj commentedon Apr 15, 2020
@anchann Thanks a lot for being open to support this project!
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 😄
anchann commentedon Apr 16, 2020
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
IsUiVisiblefunction that takes a concrete instance does not apply to this problem at all.anchann commentedon Apr 16, 2020
@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.
anchann commentedon Apr 16, 2020
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.fsessentially just routes [reflection-related for our purposes here] calls to a typescript fileReflection.ts, which is the JS runtime implementation of these calls. In order to respond to these queries, the functions inReflections.tsneed 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 thatReflection.tsgets to work with.The
transformUnionReflectionInfomethod in theFable2Babel.fsfile 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 theGetCustomAttributesrequest. (I'm taking it one step at a time, only dealing with attributes on union cases, and later will deal with other attributable entities)GetCustomAttributestakes 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.fslevel, 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
Armed with this, inside
Reflection.tswe 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 atypeName: stringto 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 ofReflection.ts, dynamically, give us access to this: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
FSharpAttributetype'sConstructorArgumentstype isIList<FSharpType * obj>. I've no idea what theobjactually 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! :)