Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# How to write a function that takes int list or string list

Tags:

f#

I'm messing around in F# and tried to write a function that can take an int list or a string list. I have written a function that is logically generic, in that I can modify nothing but the type of the argument and it will run with both types of list. But I cannot generically define it to take both.

Here is my function, without type annotation:

let contains5 xs =
    List.map int xs
    |> List.contains 5

When I try to annotate the function to take a generic list, I receive a warning FS0064: the construct causes the code to be less generic than indicated by the type annotations. In theory I shouldn't need to annotate this to be generic, but I tried anyway.

I can compile this in two separate files, one with

let stringtest = contains5 ["1";"2";"3";"4"]

and another with

let inttest = contains5 [1;2;3;4;5]

In each of these files, compilation succeeds. Alternately, I can send the function definition and one of the tests to the interpreter, and type inference proceeds just fine. If I try to compile, or send to the interpreter, the function definition and both tests, I receive error FS0001: This expression was expected to have type string, but here has type int.

Am I misunderstanding how typing should work? I have a function whose code can handle a list of ints or a list of strings. I can successfully test it with either. But I can't use it in a program that handles both?

like image 602
greggyb Avatar asked Aug 08 '19 22:08

greggyb


3 Answers

You are running into value restrictions on the automatic generalization of the type inference system as outlined here

Specifically,

Case 4: Adding type parameters.

The solution is to make your function generic rather than just making its parameters generic.

let inline contains5< ^T when ^T : (static member op_Explicit: ^T -> int) > (xs : ^T list)  =
    List.map int xs
    |> List.contains 5

You have to make the function inline because you have to use a statically resolved type parameter, and you have to use a statically resolved type parameter in order to use member constraints to specify that the type must be convertible to an int. As outlined here

like image 92
Steve Avatar answered Oct 27 '22 20:10

Steve


You can use inline to prevent the function from being fixed to a particular type.

In FSI, the interactive REPL:

> open System;;
> let inline contains5 xs = List.map int xs |> List.contains 5;;
val inline contains5 :
  xs: ^a list -> bool when  ^a : (static member op_Explicit :  ^a -> int)

> [1;2;3] |> contains5;;
val it : bool = false

> ["1";"2";"5"] |> contains5;;
val it : bool = true

Note that the signature of contains5 has a generic element to it. There's more about inline functions here.

like image 7
Curt Nichols Avatar answered Oct 27 '22 22:10

Curt Nichols


This is already answered correctly above, so I just wanted to chime in with why I think it's a good thing that F# appears to makes this difficult / forces us to lose type safety. Personally I don't see these as logically equivalent:

let inline contains5 xs = List.map int xs |> List.contains 5

let stringTest = ["5.00"; "five"; "5"; "-5"; "5,"]
let intTest = [1;2;3;4;5]

contains5 stringTest // OUTPUT: System.FormatException: Input string was not in a correct format.
contains5 intTest // OUTPUT: true

When inlined, the compiler would create two logically distinct versions of the function. When performed on the list<int> we get a boolean result. When performed on a list<string> we get a boolean result or an exception. I like that F# nudges me towards acknowledging this.

let maybeInt i = 
    match Int32.TryParse i with
    | true,successfullyParsedInteger -> Some successfullyParsedInteger
    | _ -> None

let contains5 xs = 
    match box xs with
    | :? list<int> as ixs -> 
        ixs |> List.contains 5 |> Ok
    | :? list<string> as sxs -> 
        let successList = sxs |> List.map maybeInt |> List.choose id
        Ok (successList |> List.contains 5)
    | _ -> 
        Error "Error - this function expects a list<int> or a list<string> but was passed something else."

let stringTest = ["5.00"; "five"; "5"; "-5"; "5,"]
let intTest = [1;2;3;4;5]

let result1 = contains5 stringTest // OUTPUT: Ok true
let result2 = contains5 intTest // OUTPUT: Ok true

Forces me to ask if some of the values in the string list cannot be parsed, should I drop out and fail, or should I just try and look for any match on any successful parse results?.

My approach above is horrible. I'd split the function that operates on the strings from the one that operates on the integers. I think your question was academic rather than a real use case though, so I hope I haven't gone off on too much of a tangent here!

Disclaimer: I'm a beginner, don't trust anything I say.

like image 3
drkmtr Avatar answered Oct 27 '22 20:10

drkmtr