2
votes

I've distilled these sample types from my design:

type SomeType = T1 of int | T2 of string
type Condition = Int | String

Now, for unit testing purposes and other reasons that were distilled from here(design can't be changed), I must create SomeType values based on Condition tag.

However, my first attempt didn't compile:

let inline createSomeType' (someTypeVal : ^T, cond) =
  match cond with
  | Int -> T1 someTypeVal    // warning FS0064: 'T restricted to "int"
  | String -> T2 someTypeVal // error FS0001: expected "string" got "int"

Changing function signature to createSomeType'< ^T > didn't help either:

error FS0001: expected "int" got 'T
error FS0001: expected "string" got 'T

Then I tried overloading:

type detail = 
  static member inline dispatch (someTypeVal : int) = T1 someTypeVal
  static member inline dispatch (someTypeVal : string) = T2 someTypeVal

let inline createSomeType' (someTypeVal : ^T, cond) =
  match cond with
  | Int -> detail.dispatch someTypeVal    // error FS0041: ambiguous overload
  | String -> detail.dispatch someTypeVal // error FS0041: ambiguous overload

Let's disambigue, right? No, adding type annotations restricts someTypeVal to int and we're back where we started.

From C++ point of view, all that means that F# compiler doesn't support SFINAE on union cases in pattern matching. We could use quotations or dynamic checking like this:

A)

let inline createSomeType ((someTypeVal : obj), cond) = 
  match box someTypeVal with
  | :? int when cond = Int -> T1(someTypeVal :?> int)
  | :? string when cond = String -> T2(someTypeVal :?> string)
  | _ -> failwith "something happened:("

B)

type Condition = Int = 0 | String = 1

type detail = 
  static member inline dispatch ((someTypeVal : int), (c : int)) = 
    if Condition.Int = enum<Condition>(c) then
      T1 someTypeVal
    else
      failwith "something happened:("
  static member inline dispatch ((someTypeVal : string), (c : int)) = 
    if Condition.String = enum<Condition>(c) then
      T2 someTypeVal
    else
      failwith "something happened:("

detail.dispatch(123, int Condition.Int) // usage

However, that's not concise and throws exceptions.

How should I implement createSomeType function so it does everything at compile-time?

P.S. The question is intentionally detailed because I couldn't find much info on this subject in one place, so someone googling will save time not repeating my errors.

EDIT:

Basically, I needed a single convenient function which uses both cond and someTypeVal with a signature like ^TVal -> Condition -> SomeType and compile time type resolution.

As @Gustavo says, IIUC, it's not possible without writing N * M overloads:

you can't expect the compiler to check for the cases contained in cond, since those cases are values.

1
Is it not clear to me this: "create SomeType values based on Condition tag." is it really like that or is it based on the type of someTypeVal? It seems redundant, please clarify your intention. - Gus

1 Answers

5
votes

As stated in the comments it is not clear for me what do you want to achieve. What's deciding? The VALUE of cont or the TYPE of SomeType?

All errors you're getting make totally sense to me. In the first attempt, the first case of the match assumes you receive an integer, so it unifies with integer. Otherwise the value should come boxed, like this:

let createSomeType' (someTypeVal : obj, cond) =
  match cond with
  | Int    -> T1 (someTypeVal :?> int   )
  | String -> T2 (someTypeVal :?> string)

In the second attempt the problem is that overloading in F# doesn't work like that. It tries to resolve at the call site, unless the overload involves static constraints, which in this case doesn't.

This code will work:

type SomeType  = T1 of int | T2 of string
type Condition = Int | String

type Detail = Detail with
  static member ($) (Detail, someTypeVal : int) = T1 someTypeVal
  static member ($) (Detail, someTypeVal : string) = failwith "something went wrong"; T2 someTypeVal
  static member (%) (Detail, someTypeVal : string) = T2 someTypeVal
  static member (%) (Detail, someTypeVal : int) = failwith "something went wrong"; T1 someTypeVal


let inline createSomeType' (someTypeVal : ^T, cond) =
  match cond with
  | Int    -> Detail $ someTypeVal
  | String -> Detail % someTypeVal

You can expect the compiler to check on the type of someTypeVal because it's a type and it will be checked at compile-time, but you can't expect the compiler to check for the cases contained in cond, since those cases are values.

A common misconception is that cases of a Discriminated Union represent types, in fact they represent different values on a single type.

If you will rely on types you don't need cond at all. Then your code will be:

type SomeType  = T1 of int | T2 of string

type Detail = Detail with
  static member ($) (Detail, someTypeVal : int) = T1 someTypeVal
  static member ($) (Detail, someTypeVal : string) = T2 someTypeVal


let inline createSomeType' (someTypeVal : ^T) = Detail $ someTypeVal

I used operators instead of named methods because they infer the signature with the static constraints automatically, but you can use named methods as well.