F# .Net Editor Examples

F# .Net Editor Examples

zamakzamak Posts: 10Questions: 1Answers: 0

I've been using DataTables for around 2 years now and it's been fantastic! We're currently switching from PHP over to F# and I know there is a .Net version written in C#, but I was wondering if there were any examples of it working with F#. They're fairly interoperable so I imagine it's basically a 1:1 translation, but having a reference for more complex scenarios is really nice.

Answers

  • allanallan Posts: 63,494Questions: 1Answers: 10,470 Site admin

    I'm sorry to say we don't have any examples. I've actually never written any F# myself (its on my todo list, along with Go and Rust), however, due to the Common Runtime Environment in .NET the DataTables.dll will still be useful in F#, I'm just not sure what the surrounding syntax will be I'm afraid.

    If you do cook anything up, perhaps you could post it so others could see and benefit from it as well?

    Thanks,
    Allan

  • zamakzamak Posts: 10Questions: 1Answers: 0
    edited April 2022

    It's been a bit of a slog. Not only am I using F#, I am running on .Net 6 -- so .net core -- and I am using an F#-specific framework called Falco. It was a lot of research to figure out how to wire everything up, but I did it. Although, I've encountered a problem I haven't been able to solve.

    The program type-checks and compiles, however when I hit the route, it doesn't send any data back. It's just a completely empty response. Any ideas?

    I should note that I know the request from the client is formatted correctly because I've just re-pointed the existing frontend from our PHP backend to the new F# backend.

    Editor.fs

    //Initializing the DBs
    let db1 = new Database("sqlserver", Orm.Connection1)
    let db2 = new Database("mysql", Orm.Connection2)
    
    //Initializing the Editor
    let tireDataEditor ( request : HttpRequest ) = 
        Editor( db2, "tires.tireData", pkey="sku" )
            .Field(
                [
                    Field( "sku" )
                    Field( "brandId" )
                    Field( "modelName" )
                    Field( "sizeDisplay" )
                    Field( "mfgNo" )
                    Field( "section" )
                    Field( "aspect" )
                    Field( "rim" )
                    Field( "loadIndex" )
                    Field( "loadRange" )
                    Field( "speedRating" )
                    Field( "tireType" )
                    Field( "tireSubType" )
                    Field( "season" )
                    Field( "mfgWarranty" )
                    Field( "description" )
                    Field( "features" )
                    Field( "benefits" )
                    Field( "runFlat" )
                    Field( "activeFlag" )
                    Field( "retailFlag" ) 
                ]
            )
            .Process( request.Form )
            .Data()
    

    Program.fs

    //Registering Factories
    DbProviderFactories.RegisterFactory("System.Data.SqlClient", SqlClientFactory.Instance)
    DbProviderFactories.RegisterFactory("MySqlConnector", MySqlConnectorFactory.Instance)
    ...
    //Wiring up the route to the editor
    let inline handleDataTable variable fallback = fun ctx -> 
        let reader = Request.getRoute ctx     
        let response = 
            reader.GetString variable fallback 
            |> function //This is were I would match on route, but only using 1 right now
                | _ -> tireDataEditor ctx.Request
                    
         ctx 
         |> addCors
         |> Response.ofJsonOptions options response
    ...
    //Declaring the route
    Routing.any
        "/automation/data/{table}"
        ( handleDataTable "tireData" "tireData" )
    
  • allanallan Posts: 63,494Questions: 1Answers: 10,470 Site admin

    What happens to tireDataEditor in Editor.fs? Does it get exported, or something else? Or can Program.fs access it from line 11 in there? That's not something I'm familiar with I'm afraid, but from experience with other languages and frameworks I'd expect Editor.fs to need to return or export something.

    Allan

  • zamakzamak Posts: 10Questions: 1Answers: 0

    Ah yes, this is common with ML-family languages like F#, OCaml, Haskell, SML, the language is expression based so the function gets assigned to the value of the last expression. There is no "return" keyword... except in special use-cases like in Asynchronous/Task programming. F# is also white-space sensitive so that's why there's also a lot of indentation. If you're familiar with python it should look pretty similar. Essentially, replace let with def.

    Program.fs has Editor.fs in-scope and, like you said, is calling tireDataEditor on line 11.

  • zamakzamak Posts: 10Questions: 1Answers: 0
    edited April 2022

    I've discovered an error message. I thought I was printing off the entire object, but I guess it wasn't.

    "The specified invariant name 'MySql.Data.MySqlClient' wasn't found in the list of registered .NET Data Providers."

    What's strange is that I am not using MySql.Data.MySqlClient. I'm using MySqlConnector and I never registered that.

  • zamakzamak Posts: 10Questions: 1Answers: 0

    I'm guessing the library is hard-coded to use MySql.Data.MySqlClient I registered that, but since I was using MySqlConnector, there was a casting issue.

    I'm pretty sure the consensus is the MySqlConnector will be the de facto mysql driver for .net going forward. You might want to update the library to allow for MySqlConnector instead. It'd be great if instead of that, you could just provide any arbitrary driver that implement the DbFactory pattern.

  • allanallan Posts: 63,494Questions: 1Answers: 10,470 Site admin

    If you use the "mysql" option to create a Database() instance, then yes, that is the connector that it attempts to use.

    You could use this constructor to specify a different connector.

    public Database(string dbType, string str, string adapter = null)
    

    Ah yes, this is common with ML-family languages

    Ah - so are variables in other files not local? They are automatically exported and available for reference in other files?

    Allan

  • zamakzamak Posts: 10Questions: 1Answers: 0

    No, I guess I misunderstood your question. Variables are local and behind namespaces, modules, and types. F# has open statements that are equivalent to C#'s use statements. Since F# has full OOP support, you can declare an object with private fields/methods if you wanted to, basically the same way you would in C#. I think you can also do it with module functions as well, though I've never had the use-case for it. Since everything is immutable by default, having things be private really isn't a big concern since state is hard to change anyway.

    As an example, if the Editor class was written in F#, a more idiomatic way to do would be:

    namespace MyNamespace 
    module ACollectionOfRelatedFunctionsTypesAndClasses = 
        type Editor = 
            { Fields: Field list }
            member private this.CanOnlyBeCalledFromEditor () = //Takes "()" (unit) as argument to note that it produces a side-effect, mutating state somewhere in the system
                 expression(s) 
    
            static member field fieldToAdd state = 
                this.CanOnlyBeCalledFromEditor ()
                let newFieldList = List.append state.Fields [ fieldToAdd ] //Constructs a new list with added field 
                { state with Fields = newFieldList } //Copies object with new list set to be value of Fields
    
            static member process someArg state = 
                { state with SomeArge = someArg } //See "field"
    
            static member data state = 
                expression(s)
        ...
    //Somewhere else in the project, but under the same namespace
    namespace MyNamespace 
    module MyDomainModel = 
        open ACollectionOfRelatedFunctionsTypesAndClasses
        let myEditor someArg = 
            Editor.new db2 "tires.tireData" "sku" 
            |> Editor.field Field("MyField") //This |> (pipe operator) takes the output from the line above and puts it into the last argument for this line
            |> Editor.CanOnlyBeCalledFromEditor //This can't be done. Not only does it not exist on the type, even if trying to call it on instance would fail. 
            |> Editor.process someArg 
            |> Editor.data
    

    State is explicitly passed around and very rarely modified in-place so accessibility modifies become significantly less relevant. That's not to say they are never used, because they do have their place, they just don't really apply in the functional coding style.

  • zamakzamak Posts: 10Questions: 1Answers: 0

    I got it connected with the hard-coded MySql client. However I am still not getting any data. Currently just exploring the values from the returned object.

  • zamakzamak Posts: 10Questions: 1Answers: 0

    I can confirm that the DtResponse object does indeed hold the correct data from the DB. It's just something between calling it and consuming the object to be formatted into Json that there is an issue.

  • allanallan Posts: 63,494Questions: 1Answers: 10,470 Site admin

    Many thanks for the run down there. I really must get around to trying it out sometime! I love functional programming in Javascript.

    I'm afraid I don't have any suggestions for that final step there - if it is getting the JSON object but the controller isn't sending it back to the client-side, something in the controller? Be very interesting to know if you get a solution.

    Allan

  • zamakzamak Posts: 10Questions: 1Answers: 0

    It honestly blew my mind at first what you can do with it.

    F# is a nice middle-ground between the pedanticness of Haskell and the unsafety of C#. I don't think it's a fair comparison to see functional programming in an imperative/oo language like JavaScript to a language where it's first-class.

    Like with C#/Java's idea that everything is an object, in F# everything is a function. Everything takes a function and returns a function. This allows for functions to be deconstructed and reconstructed on-the-fly and gives us something called currying.

    //This basically means return a function that takes two parameters and 
    //once both parameters are satisfied, execute the following expressions
    let add left right = 
        left + right 
    
    //What happens here is that "left" gets bound to "2" and then add2 
    //returns a function that takes a single argument which will get bound 
    //to "right" when it's called. 
    let add2 = add 2 
    
    // evaluates to 6
    let myValue = add2 4 
    

    It's hard to really show how just that is super powerful with out a lot of context, but this basically sets you up for a single-function for all dependency injection needs. Couple that with function composition or pipe operators and there's so much code clutter that gets eliminated. No more intermediate variables necessary!

    let add left right = left + right 
    let multiply left right = left * right 
    let add2 = add 2 
    let multiplyBy4 = multiply 4 
    
    // Here the ">>" operator is function composition
    // this is defined as " let compose f g x = g ( f ( x ) ) "
    // so what's returned is a function that adds2 first 
    // and then multiplies by 4 there's also "<<" which 
    // does the same thing, but reverse the order. 
    // So this function only needs to take a single argument 
    // and then the other functions sort of "inject" their 
    // expressions into it.
    let add2ThenMultiplyBy4 =  add2 >> multiplyBy4
    

    I'm no expert with JS, but my experience with it, trying to do anything like the above is a significant pain and would require a lot more code.

    Still haven't found a solution yet unfortunately, though I'm still digging.

  • allanallan Posts: 63,494Questions: 1Answers: 10,470 Site admin

    Awesome - thank you. It is possible to curry in Javascript, but you are right, in F# functional programming is what the language is all about, so anything else is going to look poor compared to it :).

    Allan

  • zamakzamak Posts: 10Questions: 1Answers: 0

    Ok so I think I've been able to get a work-around. There is something strange going on with the built-in serializer. I am not sure why, but just passing the object directly from the .Data() method on the editor to Falco's serializer, which is defined as

    https://github.dev/pimbrouwers/Falco
    let ofJsonOptions (options : JsonSerializerOptions) (obj : 'a) : HttpHandler =
        let jsonHandler : HttpHandler = fun ctx ->
            #if NETCOREAPP3_1 || NET5_0
            unitTask {
            #else
            task {
            #endif
                use str = new MemoryStream()
                do! JsonSerializer.SerializeAsync(str, obj, options = options)
                str.Flush ()
                let bytes = str.ToArray ()
                let byteLen = bytes.Length
                ctx.Response.ContentLength <- Nullable<int64>(byteLen |> int64)
                let! _ = ctx.Response.BodyWriter.WriteAsync(ReadOnlyMemory<byte>(bytes))
                return ()
            }
    
        withContentType "application/json; charset=utf-8"
        >> jsonHandler
    

    just does not work. So what I did to get it to work is map the DtResponse object in an anonymous record with the exact same fields as the DtResponse object.

    match tableName with 
    | _ -> tireData ctx.Request |> editor MySql
    |> fun x ->
        {| data = x.data; options = x.options; files = x.files; searchPanes = x.searchPanes; draw = x.draw; recordsTotal = x.recordsTotal; recordsFiltered = x.recordsFiltered |} 
    |> Response.ofJson
    

    Now everything seems to be working! I have opened an issue on the Falco project to see if anyone else has experienced issues with serializing objects but for right now. I think the working solution was my 2nd post, we've just been chasing some weird bug.

    Hopefully anyone needing examples can use this as a reference in the future.

  • allanallan Posts: 63,494Questions: 1Answers: 10,470 Site admin

    That's superb - many thanks for sharing this with us :)

    Allan

Sign In or Register to comment.