leflings
leflings

Reputation: 646

Type mismatch in seemingly equal use cases

I'm creating a module to access a MySQL database. Since SQLProvider fell short in needed functionality, and since said functionality is in fact pretty simple, I decided to roll my own based on the usual ADO.NET SqlConnection.

To make using the API less clunky, I used Tomas Petriceks idea with some slight modifications

The following shows the central parts of problem I have

let internal connectionFor ta =
    let conn = new DynamicSqlConnection(connectionStringFor ta)
    conn.Open()
    conn

let executeQuery queryString parameters map ta =
    use conn = connectionFor ta
    use cmd = conn %% queryString
    List.iter (fun setter -> setter cmd) parameters
    use r = cmd.ExecuteReader()
    [| while r.Read() do yield map r |]

let getKolSkillInterests ta =
    let map r =
        {   Id = r?id
            KolId = r?kol_id
            SkillId = r?skill_id
            SkillInterestTypeId = r?skill_interests_types_id }
    executeQuery "SELECT * FROM tb_kol_skills_interests" [] map ta

let getCountryById ta i =
    let query = "SELECT * FROM tb_lu_country WHERE id = @countryId"
    let parameters = [ fun (c:DynamicSqlCommand) -> c?countryId <- i ]
    let map r =
        { Id = r?id
          Label = r?label
          RegionId = r?region
          Abbreviation = r?country_abbreviation
          RealCountry = bigintToBool r?real_country }
    executeQuery query parameters map ta
                                  ^^^ Error here

The executeQuery function works well, when used in interactive mode, however in the above snippet, while getKolSkillInterests compiles and works, the getCountryById function will not compile. The squiglly marks occur under the map parameter to executeQuery with the error Type mismatch. The type PrintfFormat<'a,'b,'c','a,unit> is not compatible with the type string

I know F# has pretty cryptic error messages at times, but I cannot grok this one.

Note, if I inline the parameters, as shown below - it works

let getCountryById ta (i:int) =
    executeQuery
        "SELECT * FROM tb_lu_country WHERE id = @countryId"
        [fun (c:DynamicSqlCommand) -> c?countryId <- i]
        (fun r ->
            { Id = r?id
              Label = r?label
              RegionId = r?region
              Abbreviation = r?country_abbreviation
              RealCountry = bigintToBool r?real_country })
        ta

Upvotes: 1

Views: 110

Answers (2)

nodakai
nodakai

Reputation: 8001

I'm not saying there's type mismatch. F# type inference cannot type all well typed F# code without some explicit annotation because of too much deviation from classical Hindley-Milner-Damas (Also Google for "F# left to right inference".)

I see your error msg contains PrintfFormat which, I suspect, originated from MySQL binding, so I won't go into the full details. But I'd say (s)printf of F# is complex enough that whether you gave type annotation or not might change how type inference goes.

Please recall that inlining function arguments which are themselves polymorphic is equivalent to adding some type annotations to them, because when their types are inferred, you already have the surrounding context (executeQuery in this case whose type was, as you observed, already inferred.) Again, top-to-bottom and left-to-right inference.

This is why I'm concerned with giving more and more type annotations.

Upvotes: 1

Fyodor Soikin
Fyodor Soikin

Reputation: 80744

These cases are not actually "equal".

I don't exactly know what operator %% does, but from your error message it is clear that it expects a PrintfFormat<...> as second argument, and therefore, through the magic of type inference, the queryString parameter of your executeQuery function is also of type PrintfFormat<...>.

Now, this type PrintfFormat<...> is special. It is so special that the compiler itself provides a hack (of sorts) when compiling it, and that's how you can use printf. Try this:

printf "Abcd %d" 15  // Works just fine

let s = "Abcd %d"
printf s 15  // Same error as you're getting

Why is that? Turns out, in F#, string literals are not always of type string. Sometimes, depending on the context, they can be of type PrintfFormat<...>. If the compiler sees that the context requires PrintfFormat<...>, it will compile string literal as that instead of string. That's why the first printf call above works and the second one doesn't: the symbol s has already been inferred as string, and so it doesn't work as argument for printf, which is expected to be PrintfFormat<...>.

The example above is easily fixable - you just need to give the compiler a hand in figuring out your intended types:

let s: PrintfFormat<_,_,_,_> = "Abcd %d"
printf s 15  // Works now

As so your code is similarly fixable:

let getCountryById ta i =
    let query: PrintfFormat<_,_,_,_> = "SELECT * FROM tb_lu_country WHERE id = @countryId"
    let parameters = [ fun (c:DynamicSqlCommand) -> c?countryId <- i ]
    let map r =
        { Id = r?id
          Label = r?label
          RegionId = r?region
          Abbreviation = r?country_abbreviation
          RealCountry = bigintToBool r?real_country }
    executeQuery query parameters map ta

P.S. Of course, since you're not actually using the PrintfFormat capabilities, you may want to redesign your API so that it takes a string, thus making it easier for the consumer.

Upvotes: 2

Related Questions