[DajSięPoznać#12] F# Type Providers – przejęcie kontroli nad IntelliSense

Wstęp

Muszę przyznać, że kiedy pierwszy raz zobaczyłem na KGD.NET prezentację o F# to Type Providers zrobiły na mnie duże wrażenie. Typy danych wraz z metodami i propercjami generowanymi “w locie” na podstawie connection stringa do bazy czy url do REST-owego serwisu wyglądały nieco  magicznie. Community wokół F# zbudowało sporo takich providerów dostępnych jako paczki NuGet-owe.

Postanowiłem też napisać swój własny. Jego statycznym parametrem będzie nazwa polskiego miasta i wokół niej na podstawie danych z wikipedii zostanie zbudowany obiekt z propercjami zasilonymi podstawowymi danymi na temat dzielnic tego miasta.

Code Quotations

Type providers wykorzystują mechanizm code quotations będący odpowiednikiem drzew wyrażeń z C#. Ich zadaniem jest budowa abstract syntax tree (AST), przetwarzanego w przypadku type providerów “w locie”, tak, by zasilić danymi IntelliSense. Code quotations najczęściej tworzy się poprzez quoted literals. Te z podwójnymi małpami nie reprezentują typu, natomiast w przypadku małp pojedynczych, typ również musi zostać wyspecyfikowany. Przykład:

<@@ fun x y -> x*y  @@>

Autoopen i helpery

Tworzenie własnego providera warto rozpocząć od doinstalowania startera dostarczającego zestaw pomocniczych funkcji.

abc

Dodatkowo swoją logikę umieszczam w auto-otwierającym się module, tak, by funkcje były dostępne od razu po załadowaniu dll-ki przez Visual Studio. W tym przypadku, kod zasilający danymi dostarczane typy będzie parsował tabelki z wikipedii.

[<AutoOpen>]
module ProviderHelpers

open FSharp.Data
open ProviderImplementation.ProvidedTypes


let tableRowToMap(trs:seq<HtmlNode>) = 
    let header = trs |> Seq.map(fun tr -> tr.Descendants("th"))
                     |> Seq.find(fun th -> (th |> Seq.length > 0))
                     |> Seq.map(fun th -> th.InnerText())

    trs |> Seq.map(fun tr -> (tr.Descendants("a") |> Seq.tryHead, 
                                tr.Descendants("td") 
                                |> Seq.map(fun t -> t.InnerText())))
        |> Seq.filter(fun pair -> fst(pair) |> fun a -> a.IsSome)
        |> Seq.map(fun p -> (fst(p)
                                |> fun a-> a.Value.InnerText(), 
                                            snd(p) |> Seq.zip header))
        |> Map.ofSeq

    

let getDistricts(city) = 
    let city = city |> cityFromBaseString
    HtmlDocument.Load("https://pl.wikipedia.org/wiki/Podzia%C5%82_administracyjny_"+ city)
        .Descendants("table") |> Seq.head
        |> fun item -> item.Descendants("tr")
        |> tableRowToMap

Type Provider

[<TypeProvider>]
type DistrictProvider() as this = 
   inherit TypeProviderForNamespaces()

   let assembly = System.Reflection.Assembly.GetExecutingAssembly()
   let ns = "Byteville.DistrictProvider"
      
   let districtProviderType = 
        ProvidedTypeDefinition(assembly, ns, "DistrictProvider", None)

   let instantiate typeName ([|:? string as city|]: obj array) =
       let ty = ProvidedTypeDefinition(assembly, ns, typeName, None)

       let ctor = ProvidedConstructor(List.Empty,
                    InvokeCode = fun [] -> <@@ city |> getDistricts @@>)

       ctor.AddXmlDocDelayed(fun () -> "Creates an instance of provider by given city")

       ty.AddMember ctor

       city |> getDistricts
            |> Seq.map(fun mapItem ->
                ProvidedProperty(mapItem.Key, typeof<Map<string,string>>,
                    GetterCode = fun [map] ->
                        let key = mapItem.Key
                        <@@ ((%%map:obj) :?> Map<string, seq<string*string>>).[key] |> Map.ofSeq @@>))
            |> Seq.toList
            |> ty.AddMembers      
       
       ty

   do
       districtProviderType.DefineStaticParameters(
        [ProvidedStaticParameter("city", typeof<string>)], 
            instantiate)

   do
        this.AddNamespace(ns, [districtProviderType])

[<assembly:TypeProviderAssembly>]
    do()

 

Rozpoczynamy od atrybutu <TypeProvider> będącego instrukcją dla kompilatora odnośnie tego, jak ma ten typ traktować. Sam typ przyjmuje statyczny parametr będący nazwą miasta i generuje dla niego tyle propercji instancyjnych ile dzielnic ma dane miasto. Wartością każdej z takich propercji będzie mapa atrybutów i ich  wartości.

Advertisements

One thought on “[DajSięPoznać#12] F# Type Providers – przejęcie kontroli nad IntelliSense

  1. Pingback: [DajSięPoznać] Podsumowanie | When the smoke is going down

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s