Fable: Applying functions declared with point-free style fails

Created on 22 Oct 2017  Â·  46Comments  Â·  Source: fable-compiler/Fable

Description

I am trying to use following code from https://fsharpforfunandprofit.com/posts/elevated-world-3/
and it is not working correctly in Fable, it works fine in dotnet core.

module AppFunc

open System

let (<!>) = Result.map


let apply fResult xResult = 
    match fResult,xResult with
    | Ok f, Ok x ->
        Ok (f x)
    | Error errs, Ok x ->
        Error errs
    | Ok f, Error errs ->   
        Error errs
    | Error errs1, Error errs2 ->
        // concat both lists of errors
        Error (List.concat [errs1; errs2])

let (<*>) = apply

and I am using is as shown below

let private createLoginInternal username password = 
    {UserName = username; Password = password }

let createLogin username password =
    createLoginInternal <!> (Email.create username) <*> (Password.create password)

Type Email and Password are declared as

type Email = EmailType of string
with 
    static member create = function
        | x when String.IsNullOrEmpty(x) -> Error ["Email must not be empty."]
        | x when not (x.Contains("@")) -> Error ["Email must contain '@'."]
        | x when not (x.Contains(".")) -> Error ["Email must contain '.'."]
        | x -> Ok(EmailType x)

    member this.Value =
        let _val (EmailType x) =
            x

        _val this

type Password = PasswordType of string
with 
    static member create = function
        | x when String.IsNullOrEmpty(x) -> Error ["Password must not be empty."]
        | x when x.Length < 5 -> Error ["Password must have atleast 5 characters."]
        | x -> Ok(PasswordType x)

    member this.Value =
        let _val (PasswordType x) =
            x

        _val this

Calling create login from Suave app as shown below works fine

createLogin "[email protected]" "masterkey"

but fails for Fable with error Unable to process a message: TypeError: matchValue[0].data is not a function

Most helpful comment

Reproducible without any external dependencies in the REPL with Option:

open System

let apply f x =
  match f, x with
  | Some f, Some x -> Some (f x)
  | _ -> None

let (<!>) = Option.map
let (<*>) = apply

type Login =
  { Name : Name
    Password : Password }

and Name =
  | Name of string

  static member create = function
    | s when not (String.IsNullOrEmpty s) -> Some (Name s)
    | _ -> None

and Password =
  | Password of string

  static member create = function
    | s when String.length s >= 5 -> Some (Password s)
    | _ -> None

let private createLoginInternal name password =
  { Name = name; Password = password }

let createLogin name password =
  createLoginInternal <!> (Name.create name) <*> (Password.create password)

createLogin "jd" "secret"
|> printfn "%A"

However, it's easily fixable with this change:

   | _ -> None

-let (<!>) = Option.map
+let (<!>) f x = Option.map f x
 let (<*>) = apply

 type Login =

@sandeepc24 Can you please try to do this:

-let (<!>) = Result.map
+let (<!>) f x = Result.map f x

All 46 comments

Reproducible without any external dependencies in the REPL with Option:

open System

let apply f x =
  match f, x with
  | Some f, Some x -> Some (f x)
  | _ -> None

let (<!>) = Option.map
let (<*>) = apply

type Login =
  { Name : Name
    Password : Password }

and Name =
  | Name of string

  static member create = function
    | s when not (String.IsNullOrEmpty s) -> Some (Name s)
    | _ -> None

and Password =
  | Password of string

  static member create = function
    | s when String.length s >= 5 -> Some (Password s)
    | _ -> None

let private createLoginInternal name password =
  { Name = name; Password = password }

let createLogin name password =
  createLoginInternal <!> (Name.create name) <*> (Password.create password)

createLogin "jd" "secret"
|> printfn "%A"

However, it's easily fixable with this change:

   | _ -> None

-let (<!>) = Option.map
+let (<!>) f x = Option.map f x
 let (<*>) = apply

 type Login =

@sandeepc24 Can you please try to do this:

-let (<!>) = Result.map
+let (<!>) f x = Result.map f x

This sample is more compact:

let apply f x =
  match f, x with
  | Some f, Some x -> Some (f x)
  | _ -> None

let (<!>) f x = Option.map f x
let (<*>) = apply

let x = (+) <!> Some 40 <*> Some 2
x |> Option.iter (printfn "%i")

To make functions with more than two arguments work, we also have to do the same for <*>, otherwise it fails with the same error:

let (<!>) f x = Option.map f x
let (<*>) f x = apply f x

let add3 a b c = a + b + c
let x = add3 <!> Some 40 <*> Some 1 <*> Some 1 

-let () = Result.map
+let () f x = Result.map f x

This worked, thanks inosik

Thanks a lot for your help @inosik! Yes, point-free style sometimes confuses Fable. It's good that you made it work @sandeepc24 but I'll try to check if it can be fixed anyways. Thx for the report :)

With latest development version let (<*>) = apply is also working so maybe the issue is just fixed :smile: Though the generated JS code is much nicer when using the no point-free version (let (<*>) f x = apply f x) so I'd recommend using that one :+1:

I'll close the issue for now, please let me know if you find a similar issue.

With latest development version let (<*>) = apply is also working

I just did a full build and then a .\build.cmd QuickFableCompilerTest, and it did not work with a point-free <!> or <*>. I added this to QuickTest.fs:

let apply f x =
    match f, x with
    | Some f, Some x -> Some (f x)
    | _ -> None

let add3 a b c = a + b + c

module PointFree =
    let (<!>) = Option.map
    let (<*>) = apply
    let x = add3 <!> Some 40 <*> Some 1 <*> Some 1

module Pointful =
    let (<!>) f x = Option.map f x
    let (<*>) f x = apply f x
    let x = add3 <!> Some 40 <*> Some 1 <*> Some 1

let ``My Test``() =
    equal PointFree.x Pointful.x

It still crashes with this:

D:\projects\fable\src\tools\temp\QuickTest.js:1503
            return new Some($var1[1]($var1[2]));
                                    ^

TypeError: $var1[1] is not a function
    at apply (D:\projects\fable\src\tools\temp\QuickTest.js:1503:37)
    at D:\projects\fable\src\tools\temp\QuickTest.js:1521:20
    at curriedFn (D:\projects\fable\src\tools\temp\QuickTest.js:1466:25)
    at D:\projects\fable\src\tools\temp\QuickTest.js:1525:78
    at Object.<anonymous> (D:\projects\fable\src\tools\temp\QuickTest.js:1529:2)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)

Hmm, you're right. This is still an issue. Thanks a lot for the new test, I'll try to fix it! :+1:

@alfonsogarciacaro is this working with 3 parameters?
When are you planning to release this fix as I need it continue my development?

I have spent more time looking into this and I still get this error. Here is my code. When I call createUser I get same error.

module Domain.StringTypes

open System

let private applyValidations value okFun errorFun validations =
    let validationResults = validations
                            |> List.fold(fun a (f,m) -> if f value then m :: a else a) []
                            |> List.rev // Reverse the list as error messages are added as head element to the list.
    if List.isEmpty validationResults then
        okFun
    else
        errorFun validationResults

type String150 =
    String150Type of string
    with
        static member create = function
            | x when String.length(x) <= 150 -> Ok(String150Type x)
            | _ -> Error(["Length cannot be more than 150."])

        static member CreateNonBlank stringTitle = function
            | x when (String.length(x) > 0 && String.length(x) <= 150) -> Ok(String150Type x)
            | _ -> 
                let title = if String.IsNullOrEmpty stringTitle then "String" else stringTitle
                Error([title + " cannot be blank and must be less than 150."])

        member this.Value =
            let _val (String150Type x) =
                x

            _val this

type Email = EmailType of string
with 
    static member Create (title, email) =
        [ 
            (String.IsNullOrEmpty, title + " must not be empty.")
            ((fun x -> not (x.Contains("@"))), title + " must contain '@'.")
            ((fun x -> not (x.Contains("."))), title + " must contain '.'.")
        ] 
        |> applyValidations email (Ok (EmailType email)) Error

    static member Create email =
        Email.Create ("Email", email)

    member this.Value =
        let _val (EmailType x) =
            x

        _val this


type Password = PasswordType of string
with 
    static member Create (title, password) =
        [
            (String.IsNullOrEmpty, title + " must not be empty.")
            ((fun x -> x.Length < 5), title + " must have atleast 5 characters.")
        ] 
        |> applyValidations password (Ok (PasswordType password)) Error

    static member Create password =
        Password.Create ("Password", password)

    member this.Value =
        let _val (PasswordType x) =
            x

        _val this


type FirstName = FirstNameType of string
with 
    static member Create (title, firstName) =
        [
            (String.IsNullOrEmpty, title + " must not be empty.")
            //((fun x -> x.Length < 5), title + " must have atleast 5 characters.")
        ] 
        |> applyValidations firstName (Ok (FirstNameType firstName)) Error

    static member Create firstName =
        FirstName.Create ("FirstName", firstName)

    member this.Value =
        let _val (FirstNameType x) =
            x

        _val this


type LastName = LastNameType of string
with 
    static member Create (title, lastName) =
        [
            (String.IsNullOrEmpty, title + " must not be empty.")
            //((fun x -> x.Length < 5), title + " must have atleast 5 characters.")
        ] 
        |> applyValidations lastName (Ok (LastNameType lastName)) Error

    static member Create lastName =
        LastName.Create ("LastName", lastName)

    member this.Value =
        let _val (LastNameType x) =
            x

        _val this


type ContactName = ContactNameType of string
with 
    static member Create (title, contactName) =
        [
            (String.IsNullOrEmpty, title + " must not be empty.")
            //((fun x -> x.Length < 5), title + " must have atleast 5 characters.")
        ] 
        |> applyValidations contactName (Ok (ContactNameType contactName)) Error

    static member Create contactName =
        ContactName.Create ("ContactName", contactName)

    member this.Value =
        let _val (ContactNameType x) =
            x

        _val this


type Website = WebsiteType of string
with 
    static member Create (title, website) =
        match website with
        | Some website ->
            [
                // If website is not blank then it must start with http in it.
                ((fun (x : string) -> (x.Length > 0 && not (x.ToLower().StartsWith("http")))), title + " must have http.")
            ] 
            |> applyValidations website (Ok (Some (WebsiteType website))) Error
        | None -> Ok None

    static member Create website =
        Website.Create ("Website", website)

    member this.Value =
        let _val (WebsiteType x) =
            x

        _val this

type Phone = PhoneType of string
with 
    static member Create (title, phone) =
        match phone with
        | Some phone -> 
            [
                // If phone is not blank then it must have atleast 4 digits.
                ((fun (x : string) -> x.Length > 0 && x.Length < 5), title + " must have at least 4 digits.")
            ] 
            |> applyValidations phone (Ok (Some (PhoneType phone))) Error
        | None -> Ok None

    static member Create phone =
        Phone.Create ("Phone", phone)

    member this.Value =
        let _val (PhoneType x) =
            x

        _val this

type Mobile = MobileType of string
with 
    static member Create (title, mobile) =
        match mobile with
        | Some mobile ->
            [
                // If phone is not blank then it must have atleast 4 digits.
                ((fun (x : string) -> x.Length > 0 && x.Length < 5), title + " must have at least 4 digits.")
            ] 
            |> applyValidations mobile (Ok (Some (MobileType mobile))) Error
        | None -> Ok None

    static member Create mobile =
        Mobile.Create ("Mobile", mobile)

    member this.Value =
        let _val (MobileType x) =
            x

        _val this

type Fax = FaxType of string
with 
    static member Create (title, fax) =
        match fax with
        | Some fax ->
            [
                // If phone is not blank then it must have atleast 4 digits.
                ((fun (x : string) -> x.Length > 0 && x.Length < 5), title + " must have at least 4 digits.")
            ] 
            |> applyValidations fax (Ok (Some (FaxType fax))) Error
        | None -> Ok None

    static member Create fax =
        Fax.Create ("Fax", fax)

    member this.Value =
        let _val (FaxType x) =
            x

        _val this

`
Here is the UserDto declaration.

`
type UserDto = {
    FirstName : FirstName
    LastName : LastName
    ContactName : ContactName
    Email : Email
    Website : Website option
    Phone : Phone option
    Mobile : Mobile option
    Fax : Fax option
}

let private createUserInternal firstName lastName contactName email website phone mobile fax = 
    { 
        FirstName = firstName
        LastName = lastName
        ContactName = contactName
        Email = email
        Website = website
        Phone = phone
        Mobile = mobile
        Fax = fax
     }

let createUser firstName lastName contactName email website phone mobile fax = 
    createUserInternal <!> FirstName.Create firstName 
        <*> LastName.Create lastName 
        <*> ContactName.Create contactName 
        <*> Email.Create email
        <*> Website.Create website
        <*> Phone.Create phone 
        <*> Mobile.Create mobile 
        <*> Fax.Create fax

For info, seems like @sandeepc24 didn't update to beta version of Fable so he don't have the fix installed locally. We should wait for confirmation that the bug still exist or no but with latest beta version of Fable.

Maxime Mangel
@sandeepc24 Did you upgraded to latest beta of Fable ?
Sandeep Chandra
Not yet, how can I do that?

@sandeepc24 Sorry, I think I added the fix to the beta version, but this still has some other issues. Let me fix them before releasing a new version. After that you can upgrade it just by adding prelease in paket.dependencies like:

clitool dotnet-fable prerelease
nuget Fable.Core prerelease

Then run mono .paket/paket.exe update and dotnet restore. Or, could you please add a test as in @inosik sample so I can test your code locally? Thank you very much!

@alfonsogarciacaro - sorry to bother you about this, but I was wondering if this has been fixed?

I was wondering the same ;) Have you checked with latest version?

I updated to 1.3 and I am still having the issue :-(

Here is the image of my paket.lock file.

image

@sandeepc24 Could you please try to isolate the issue in a smaller code without dependencies? The code in this comment is quite long and it's not compiling by pasting it into a file. Also, if expected results are not provided is difficult to create a test out of it to make sure the issue won't reappear in future versions.

Here is the code in one file

#r "node_modules/fable-core/Fable.Core.dll"

open System
open System.Threading
open Fable.Core
open Fable.Import.Browser

let (<!>) = Result.map

let apply fResult xResult = 
    match fResult,xResult with
    | Ok f, Ok x ->
        Ok (f x)
    | Error errs, Ok x ->
        Error errs
    | Ok f, Error errs ->   
        Error errs
    | Error errs1, Error errs2 ->
        // concat both lists of errors
        Error (List.concat [errs1; errs2])

let (<*>) f x = apply f x

let private applyValidations value okFun errorFun validations =
    let validationResults = validations
                            |> List.fold(fun a (f,m) -> if f value then m :: a else a) []
                            |> List.rev // Reverse the list as error messages are added as head element to the list.
    if List.isEmpty validationResults then
        okFun
    else
        errorFun validationResults

type String150 =
    String150Type of string
    with
        static member create = function
            | x when String.length(x) <= 150 -> Ok(String150Type x)
            | _ -> Error(["Length cannot be more than 150."])

        static member CreateNonBlank stringTitle = function
            | x when (String.length(x) > 0 && String.length(x) <= 150) -> Ok(String150Type x)
            | _ -> 
                let title = if String.IsNullOrEmpty stringTitle then "String" else stringTitle
                Error([title + " cannot be blank and must be less than 150."])

        member this.Value =
            let _val (String150Type x) =
                x

            _val this

type Email = EmailType of string
with 
    static member Create (title, email) =
        [ 
            (String.IsNullOrEmpty, title + " must not be empty.")
            ((fun x -> not (x.Contains("@"))), title + " must contain '@'.")
            ((fun x -> not (x.Contains("."))), title + " must contain '.'.")
        ] 
        |> applyValidations email (Ok (EmailType email)) Error

    static member Create email =
        Email.Create ("Email", email)

    member this.Value =
        let _val (EmailType x) =
            x

        _val this


type Password = PasswordType of string
with 
    static member Create (title, password) =
        [
            (String.IsNullOrEmpty, title + " must not be empty.")
            ((fun x -> x.Length < 5), title + " must have atleast 5 characters.")
        ] 
        |> applyValidations password (Ok (PasswordType password)) Error

    static member Create password =
        Password.Create ("Password", password)

    member this.Value =
        let _val (PasswordType x) =
            x

        _val this


type FirstName = FirstNameType of string
with 
    static member Create (title, firstName) =
        [
            (String.IsNullOrEmpty, title + " must not be empty.")
            //((fun x -> x.Length < 5), title + " must have atleast 5 characters.")
        ] 
        |> applyValidations firstName (Ok (FirstNameType firstName)) Error

    static member Create firstName =
        FirstName.Create ("FirstName", firstName)

    member this.Value =
        let _val (FirstNameType x) =
            x

        _val this


type LastName = LastNameType of string
with 
    static member Create (title, lastName) =
        [
            (String.IsNullOrEmpty, title + " must not be empty.")
            //((fun x -> x.Length < 5), title + " must have atleast 5 characters.")
        ] 
        |> applyValidations lastName (Ok (LastNameType lastName)) Error

    static member Create lastName =
        LastName.Create ("LastName", lastName)

    member this.Value =
        let _val (LastNameType x) =
            x

        _val this


type ContactName = ContactNameType of string
with 
    static member Create (title, contactName) =
        [
            (String.IsNullOrEmpty, title + " must not be empty.")
            //((fun x -> x.Length < 5), title + " must have atleast 5 characters.")
        ] 
        |> applyValidations contactName (Ok (ContactNameType contactName)) Error

    static member Create contactName =
        ContactName.Create ("ContactName", contactName)

    member this.Value =
        let _val (ContactNameType x) =
            x

        _val this


type Website = WebsiteType of string
with 
    static member Create (title, website) =
        match website with
        | Some website ->
            [
                // If website is not blank then it must start with http in it.
                ((fun (x : string) -> (x.Length > 0 && not (x.ToLower().StartsWith("http")))), title + " must have http.")
            ] 
            |> applyValidations website (Ok (Some (WebsiteType website))) Error
        | None -> Ok None

    static member Create website =
        Website.Create ("Website", website)

    member this.Value =
        let _val (WebsiteType x) =
            x

        _val this

type Phone = PhoneType of string
with 
    static member Create (title, phone) =
        match phone with
        | Some phone -> 
            [
                // If phone is not blank then it must have atleast 4 digits.
                ((fun (x : string) -> x.Length > 0 && x.Length < 5), title + " must have at least 4 digits.")
            ] 
            |> applyValidations phone (Ok (Some (PhoneType phone))) Error
        | None -> Ok None

    static member Create phone =
        Phone.Create ("Phone", phone)

    member this.Value =
        let _val (PhoneType x) =
            x

        _val this

type Mobile = MobileType of string
with 
    static member Create (title, mobile) =
        match mobile with
        | Some mobile ->
            [
                // If phone is not blank then it must have atleast 4 digits.
                ((fun (x : string) -> x.Length > 0 && x.Length < 5), title + " must have at least 4 digits.")
            ] 
            |> applyValidations mobile (Ok (Some (MobileType mobile))) Error
        | None -> Ok None

    static member Create mobile =
        Mobile.Create ("Mobile", mobile)

    member this.Value =
        let _val (MobileType x) =
            x

        _val this

type Fax = FaxType of string
with 
    static member Create (title, fax) =
        match fax with
        | Some fax ->
            [
                // If phone is not blank then it must have atleast 4 digits.
                ((fun (x : string) -> x.Length > 0 && x.Length < 5), title + " must have at least 4 digits.")
            ] 
            |> applyValidations fax (Ok (Some (FaxType fax))) Error
        | None -> Ok None

    static member Create fax =
        Fax.Create ("Fax", fax)

    member this.Value =
        let _val (FaxType x) =
            x

        _val this

type UserDto = {
    FirstName : FirstName
    LastName : LastName
    ContactName : ContactName
    Email : Email
    Website : Website option
    Phone : Phone option
    Mobile : Mobile option
    Fax : Fax option
}

let private createUserInternal firstName lastName contactName email website phone mobile fax = 
    { 
        FirstName = firstName
        LastName = lastName
        ContactName = contactName
        Email = email
        Website = website
        Phone = phone
        Mobile = mobile
        Fax = fax
     }

let createUser firstName lastName contactName email website phone mobile fax = 
    createUserInternal <!> FirstName.Create firstName 
        <*> LastName.Create lastName 
        <*> ContactName.Create contactName 
        <*> Email.Create email
        <*> Website.Create website
        <*> Phone.Create phone 
        <*> Mobile.Create mobile 
        <*> Fax.Create fax


createUser "Sandeep" "Chandra" "Sandeep" "[email protected]" None None None None

And the output should be

 Ok {FirstName = FirstNameType "Sandeep";
      LastName = LastNameType "Chandra";
      ContactName = ContactNameType "Sandeep";
      Email = EmailType "[email protected]";
      Website = None;
      Phone = None;
      Mobile = None;
      Fax = None;}

I can't reproduce it with Fable 1.3.0, using both Webpack and Rollup as a bundler, and compiling to a Node app. And it works in the REPL as well.

I also tried to change the apply operator to point-free style, and that worked, too.

@sandeepc24 Are your JS dependencies up to date?

Changing the <!> operator to pointful style (let (<!>) f x = Result.map f x) makes it break.

Again, simplified to this:

let apply f x =
    match f, x with
    | Some f, Some x -> Some (f x)
    | _ -> None

let (<!>) mapping option = Option.map mapping option
//let (<!>) = Option.map
let (<*>) f x = apply f x
//let (<*>) = apply

let add3 a b c = a + b + c

let sum = add3 <!> Some 1 <*> Some 2 <*> Some 3
printfn "%A" sum

| <!> | <*> | Result |
|------------|------------|--------|
| Point free | Point free | OK |
| Pointful | Point free | Error |
| Point free | Pointful | OK |
| Pointful | Pointful | OK |

That's why our tests pass. Because we either use point free or pointful style, but none of the permutations.

The number of arguments changes the outcome :confused:

let add a b = a + b
let add3 a b c = a + b + c

let apply f x =
    match f, x with
    | Some f, Some x -> Some (f x)
    | _ -> None

[<CompiledName("mapOp")>]
let (<!>) mapping option = Option.map mapping option
//let (<!>) = Option.map

[<CompiledName("applyOp")>]
let (<*>) f x = apply f x
//let (<*>) = apply

try
    let (Some x) = add <!> Some 1 <*> Some 2
    printfn "add: OK (%i)" x
with
| _ -> printfn "add: Error"

try
    let (Some x) = add3 <!> Some 1 <*> Some 2 <*> Some 3
    printfn "add3: OK (%i)" x
with
| _ -> printfn "add3: Error"

| <!> | <*> | Result add | Result add3 |
|------------|------------|--------------|---------------|
| Point free | Point free | Error | OK |
| Pointful | Point free | OK | Error |
| Point free | Pointful | Error | OK |
| Pointful | Pointful | OK | OK |

Thanks a lot for all the work to investigate the issue @inosik! Really interesting results, I'll try to fix it... or definitely forbid point free style in Fable ;)

forbid point free style in Fable

How we should do ["foo"] |> List.exists ((=) "bar") then? :smile:

It seems the generated code misses some partialApply calls in the point free cases. I changed the first case (point free map and apply) to this:

try
    let add1 = add <!> Some 1
    console.log ("add1", add1)

    let (Some x) = add1 <*> Some 2
    console.log ("add: OK (", x, ")")
with
| _ -> console.log "add: Error"

The output is this:

add1 0
add: Error

While it actually should be this:

add1 (...args) => {
        // _this = _this || this;
        const actualArgsLength = Math.max(args.length, 1);
        expectedArgsLength = Math.max(expectedArgsLength || f.length, 1);
        if (actualArgsLength >= expectedArgsLength) {
            const restArgs = args.splice(expectedArgsLength);
            const res = f(...args);
            if (typeof res === "function") {
                const newLambda = CurriedLambda(res);
                return restArgs.length === 0 ? newLambda : newLambda(...restArgs);
            }
            else {
                return res;
            }
        }
        else {
            return CurriedLambda((...args2) => {
                return f(...args.concat(args2));
            }, expectedArgsLength - actualArgsLength);
        }
    }
add: OK ( 3 )

Sorry, I meant point-free style in function declarations. Until we investigate this further, I've added a warning recommending users to declare all arguments explicitly. I hope this is OK for now :)

screen shot 2017-11-21 at 11 41 32 am

I have updated to latest version of Fable (1.3.2) and I still have this issue.

This is normal the point free style isn't yet supported.

In 1.3.1 we added a warning but it's raised too much in them in different projects so we removed it in 1.3.2.

Please declare all your arguments explicitly and it should work.

I am not sure how I would do that for the test case I provided, could you please give an example of it for createUserInternal?

@sandeepc24 change your operators to pointful style, for example: let (<!>) f x = Result.map f x.

Can we detect such cases where the user simply „aliases“ a function? I think in such cases the CurriedLambda wrapping isn‘t necessary, right?

@inosik I think it's possible to detect members declared with point-free style and translate them as normal methods. But the main problem is the call site, where the AST provided by the F# compiler is very different in one case or the other. Check the difference in the F# (not Fable) AST for your sample below between point-free (left) and pointful (right) styles. Unfortunately all my attempts to detect those cases and solve all situations (like partial application, etc) have been unsuccessful 😞

screen shot 2017-11-25 at 2 13 30 pm

I'm working to restrict the warning to the problematic cases, and I think I almost have it. This will probably be the only solution for now.

@inosik I have tried that but it also fails for cases where I have more than 3 parameters.

@alfonsogarciacaro That screenshot shows the call site, what I meant was the declaration:

let add a b = a + b
let sumOf = add

Can we detect that sumOf is an alias for add? Then we could generate something like this:

function add(a, b) { return a + b; }
var sumOf = add;

Also, @sandeepc24 is right, even the pointful style doesn't work for all cases. The following snippet fails with "thenAdd2 is not a function":

let apply f x =
  match f, x with
  | Some f, Some x -> Some (f x)
  | _ -> None

[<CompiledName("op_map")>]
let (<!>) f x = Option.map f x

[<CompiledName("op_apply")>]
let (<*>) f x = apply f x

let add a b c d = a+b+c+d

try
  let add1 = add <!> Some 1
  let thenAdd2 = add1 <*> Some 2
  let thenAdd3 = thenAdd2 <*> Some 3
  let sum = thenAdd3 <*> Some 4
  printfn "%A" sum
with
| e -> printfn "%s" e.Message

Maybe JS isn't capable of working with functions as first class values at all? :smile:

@inosik Yes, we can do that, though I'm not sure it will fix all the cases because you can still declare non-alias functions with point-free style (e.g. let add5 = add 5). But precisely the point in showing the call site is that is not enough with changing the declaration, we need to track all the calls to that function and also change the AST accordingly which I haven't managed to do successfully. Note that in the JS code for this REPL sample the F# compiler is calling first op_Dollar without arguments and then applying the actual arguments one by one, this conflicts in many cases with Fable's uncurrying optimization.

I've opened an issue about this in FCS repo, let's see if they can shed some light about it: https://github.com/fsharp/FSharp.Compiler.Service/issues/838

Ah, okay, now I understand.

However, I think the real issue here isn't actually point free style, because the code in my comment above doesn't work either, and the following code works perfectly fine:

let add a b c d = a + b + c + d

let apply f x = f x

let ($) = apply

try
    let sum = add $ 1 $ 2 $ 3 $ 4
    console.log ("OK:", sum)
with
| e -> console.log ("Error:", e.Message)

Partial application seems to be somewhat broken if the function is wrapped in another type: Result in the original snippets by @sandeepc24 and Option in the repro samples. I can reproduce it with a custom Maybe<'a> type, too.

@inosik Ah, ok, I missed that. Thanks for pointing it out :+1: It's actually a different problem (the wrapping of functions with Some) it should be fixed by this commit.

Okay, this makes it work with Option :+1: But that doesn't apply to the other types, this is still reproducible with a custom type (type Maybe<'a> = Just of 'a | Nothing) and probably Result.

@inosik Result should works too because according to @alfonsogarciacaro this fixed my failing test on this project: https://github.com/MangelMaxime/Thot/issues/1#issuecomment-347179086

@MangelMaxime This seems to break when there are more than 2 arguments:

| add function | Call site | Result |
|---|---|---|
| let add a b = a+b | let sum = add <!> Ok 1 <*> Ok 2 | Ok 3 |
| let add a b c = a+b+c | let sum = add <!> Ok 1 <*> Ok 2 <*> Ok 3 | Error |

The number of arguments which makes it break looks pretty random to me, as it was working with 3 arguments with Option already. I don't recognize the pattern here yet :smile:

For completeness, this is the code I added to QuickTest.fs:

let apply (f : Result<_, unit>) (x : Result<_, unit>) =
    match f, x with
    | Ok f, Ok x -> Ok (f x)
    | _ -> Result.Error ()

let (<!>) f x = Result.map f x
let (<*>) f x = apply f x

let add a b c = a+b+c

[<Test>]
let Test () =
    let sum = add <!> Ok 1 <*> Ok 2 <*> Ok 3
    equal (Ok 6) sum

Test ()

Okay, this is crazy. When I change the operators in the code above to point free style, it works. 6 arguments and counting. :weary:

@inosik ...and that becomes test 1400 in Fable! Ok, I made another attempt at fixing the application, I hope this time it takes a bit longer until you find another failing test :wink:

Thanks @alfonsogarciacaro , can I test it by downloading the beta version of Fable?

I update to Fable 1.3.3 and I still have the issue with createUser function above.

@sandeepc24 The fix is not released yet. But I think with Fable 1.3.3 and if you change both of your operators to point free style (let (<!>) = Result.map) it should work. If not, you'd have to wait for the next release or build Fable yourself.

@sandeepc24 Fable 1.3.4 has been released, hopefully this finally fixes the issue :)

I have tested this and it is working for me now. Thank you guys so much, much appreciated.

Awesome, thanks a lot for the confirmation!

Was this page helpful?
0 / 5 - 0 ratings