Skip to content

Repository files navigation

Adaptify

Build statusDiscord

Adaptify provides a MSBuild plugin for automatically incrementalizing F# types using FSharp.Data.Adaptive. It generates associated types for all types in the project that are marked with the ModelType attribute. It currently achieves that via inserting generated files (suffixed with .g.fs) in your project that contain the adaptive reprensentations for all necessary types.

Notes

This project also contains a command line version of the generator which will be published as a dotnet tool when its done.

Note that the MSBuild plugin cannot be built on Linux/MacOS currently. Nonetheless it should work on these platforms. Currently there are efforts to improve the MSBuild integration, so in case I break something for VS/VSCode let me know...

The generated files (and their base library) are fable-compatible and the generated package includes the fable/ folder, so all of this should simply be usable in fable projects.

Example

Consider the following definitions:

// Model.fsnamespaceModel[<ModelType>]typeThing={
name :string
value :int}[<ModelType>]typeModel={
foo :int
bar :string
things :IndexList<Thing>}

Adaptify will generate two associated types that conceptually have the following signature:

// Model.g.fsnamespaceModeltypeAdaptiveThing=// all properties are adaptifiedmembername:aval<string>membervalue:aval<int>// update to a new value // (changing the contained avals/alists/etc.)memberUpdate:Thing ->unit// the current value as aval of the input-typememberCurrent:aval<Thing>// creates a new AdaptiveThing given an initial statestatic memberCreate:Thing ->AdaptiveThingtype AdaptiveModel =memberfoo:aval<int>memberbar:aval<string>memberthings:alist<AdaptiveThing>memberUpdate:Model ->unitmemberCurrent:aval<Model>static memberCreate:Model ->AdaptiveModel

Semantics

Since this type association can be quite involved at times here's a complete table showing how the translation works (where α denotes the adaptify-type-function)

ImmutableAdaptiveRemarks
α(IndexList<'T>)alist<α('T)>alist<aval<'T>> -> alist<'T>
α(HashSet<'T>)aset<α('T)>aset<aval<'T>> -> aset<'T>
α(HashMap<'K, 'V>)amap<'K, α('V)>amap<'K, aval<'V>> -> amap<'K, 'V>
α({ a:'T;.. }){ a:α('T);..}product types
α('T * ..)(α('T) * ..)tuples
α(struct('T * ..))(α('T) * ..)struct tuples
α(|A of 'T..)aval<|AdaptiveA of α('T)..>sum types
α('T)aval<'T>opaque types

Compilation

The MSBuild plugin inserts the generated files in your project directly after the original files. This means that you need to put model types in a separate file in order to see their adaptified version in subsequent files for e.g. view functions. We worked hard to avoid this, but couldn't figure out a way of working around this limitation yet. Once type providers are capable of taking types as arguments (see fsharp/fslang-suggestions#212) this limitation might be overcome.

Adaptifying third party types

Foreign types (for example from third party libraries, etc.) can be adaptified afterwards! Simply create a type alias for the foreign type to be adaptified.

[<ModelType>]typeMyTime= System.DateTime
..lett= AdaptiveMyTime(System.DateTime.Now)
t.Day |> AVal.map (fun d ->"Today is "+string d)

Namespaces

The generated types will be defined in the same namespace as your original types if the original ones were not defined inside a module.

If they are defined in a module the generated types will get defined in a somewhat strange scope. Consider:

namespaceAmoduleB =[<ModelType>]typeFoo={ bar :int }

The generator will then emit

namespaceA[<AutoOpen>]moduleAdaptify =moduleB =[<ModelType>]typeAdaptiveFoo=// ...

We sadly couldn't figure out a better way for modules, since they can only be declared once per assembly (no partial modules).
Generally speaking we strongly recommend that you put your model types directly in namespaces to avoid confusion.

Sum Types (Unions)

Union types receive special treatment and it can be irritating how they capture two kinds of changes. Therefore I'll give a short overview here.

[<ModelType>]typeUnion=| A ofint| B ofstring

Let's consider translating that in a natural way:

typeAdaptiveUnionCase=| AdaptiveA ofaval<int>| AdaptiveB ofaval<string>

This cannot capture all changes happening, since there is no adaptive reprensentation of the current union case. Therefore our system generates an additional type:

typeAdaptiveUnion=interface aval<AdaptiveUnionCase>memberupdate:Union ->unitnew :Union ->AdaptiveUnion// ...

This way we can capture both kinds of changes:

  • inner field changes are handled by the respective case-types
  • case/constructor changes are handled by the outer wrapper

Note that these aval instances will be removed whenever they occur inside changeable collections Take for example alist<AdaptiveUnion> (which is conceptually identical to alist<aval<AdaptiveUnionCase>>) will be turned into alist<AdaptiveUnionCase>, since the outer list can take care of the case changes here.

Generics

Adaptify has support for generic model-types but the compilation process is quite involved. However top-level model-types should generally not be generic, since their creation requires a lot of information users cannot easily supply themselves. Here's an example for illustration purposes: (you don't have to understand this to use the library)

[<ModelType>]typeModel<'T>={ value :'T
list :IndexList<'T>}

When instantiating Model with int the desired output is:

typeAdaptiveModelOfInt=
value : aval<int>
list : alist<int>// note that this is not alist<aval<int>>!

However when instantiating Model with a model-type Thing the desired output is:

typeAdaptiveModelOfThing=
value : AdaptiveThing
list : alist<AdaptiveThing>// same inner type like value!

To account for this kind of flexibility our compiled generic model-types replicate their type arguments s.t. the new type-arguments can be used to represent these different variants.

So the real generated type will look like:

typeAdaptiveModel<'T,'T1,'T2>=membervalue:'T2memberlist:alist<'T1>new :Model<'T> *('T -> obj)*(obj -> 'T -> obj)*(obj -> 'T1)*('T -> obj)*(obj -> 'T -> obj)*(obj -> 'T2)-> AdaptiveModel<'T, 'T1, 'T2>

As you see there are lots of functions in the constructor that account for inner changes of the generic values. For more information please see the generator code or feel free to contact us on gitter/discord. However as long as the top-level model is not generic you should never need to worry about these details.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages