1

This question seems really stupid but I can't find anything similar online. So here I go.

I need to use a Collection-like structure that should be used as values inside a Map. This collection has a generic type and the map should contain any kind of these collections (of int, boolean, string, etc). Given that this is not possible directly (because of the generic type constrained to a unique value in the Map definition) I used an auxiliary DU to match all the cases given that they are few. My problem is that I want to write a generic function that receives a HOF and execute on the Collection-like data structure regardless of the internal generic type... but I can't; this is the simple code to exemplify the problem:

type EspecificList =
    | IntList of int list
    | BoolList of bool list

let listLength = function
    | IntList list -> List.length list
    | BoolList list -> List.length list

let listGenericFunc func = function
    | IntList list -> func list
    | BoolList list -> func list

listLength work as expected but listGenericFunc doesn't; func is specialized to the first type and an error sows on the second one: adding a generic type annotation like (func: List<'a> -> 'b) doesn't work either.

Is there any way to avoid this and maintain the generic spirit of the function? I'm probably missing something really obvious but I can't see it.

Thanks in advance!

Edit

Okay so, I continued my research for the last couple of days and although I'm planning to submit a more detailed question about my particular domain problem I want to share the possible solutions that I encountered and ask If you think that any of them is better or more FSharpish. I'll list the solutions in the order that I found them.

1. WalternativE response

Passing the function n times one per possible type; with or without the type annotations

let listGenericFunc (func: bool list -> 'b) (func': int list -> 'b) = function
    | BoolList list -> func list
    | IntList list -> func' list

listGenericFunc List.length List.length (IntList [1;2])

2. Static Member and Inline magic

Using a static 'apply' member and define types for every possible function in conjunction with an 'applyer' inlined one.

type ListLength = ListLength with static member ( $ ) (ListLength, x) = x |> List.length

let inline applier f x = f $ x

let listGenericFuncInline func = function
    | IntList list -> applier func list
    | BoolList list -> applier func list

listGenericFuncInline ListLength (IntList [1; 2; 3]) // return 3
listGenericFuncInline ListLength (BoolList [true; false]) // return 2

It was a response to this particular SO question

3. Hidden Generic Type

From the last question, I found Existential types and searching a little I stumble upon this article. Using only the first part of the post about Universal types makes it possible to accomplish what I want.

type UListFuncs<'ret> = abstract member Eval<'a> : ('a list) -> 'ret

let listLength : UListFuncs<int> =
    { new UListFuncs<int> with
        member __.Eval<'a> (x : 'a list) = x |> List.length }

let listGenericFuncUniversal (func : UListFuncs<'a>) = function
| IntList list -> func.Eval list
| BoolList list -> func.Eval list

listGenericFuncUniversal listLength (IntList [1; 2; 3]) // return 3
listGenericFuncUniversal listLength (BoolList [true; false]) // return 2

Impressions

I don't know which of them is the best alternative; I feel that the second one is a bit awkward because of the type needed for every function; I really like the third regardless of the added boilerplate (The article is well explained and very interesting to read too). What are your thoughts?

Community
  • 1
  • 1

2 Answers2

3

As far as I can see it from the given code there might be a problem with non-obvious differences of the code you use.

If we look at

let listLength = function
    | IntList list -> List.length list
    | BoolList list -> List.length list

you'll see to distinct calls to List.length for every match case. The List.length function is generic and the compiler can find the correct type parameters on each call-site.

let listGenericFunc func = function
    | IntList list -> func list
    | BoolList list -> func list

This function on the other hand is a bit trickier. The compiler will try to resolve to the correct type but won't be able to do this because there is only one 'call-site' and two different cases for the type parameters. It will bind to the first case and tell you that the second one won't match. If you change the order in which you go through your match cases you'll see that the compiler will change the type signature accordingly.

A thing you could do (which I don not recommend) would be to get both cases to the same type before applying the function.

let listGenericFunc' (func: obj list -> 'b) (esp : EspecificList) =
    match esp with 
    | BoolList list -> list |> List.map box |> func
    | IntList list -> list |> List.map box |> func

This will work because your func will always have the form obj list -> b. This is not very nice, though. I'd rather change the HOF you use to have one fitting function for each case (they can also be the same generic functions - so that wouldn't cause duplication).

let listGenericFunc (func: bool list -> 'b) (func': int list -> 'b) = function
    | BoolList list -> func list
    | IntList list -> func' list

listGenericFunc List.length List.length (IntList [1;2])

Ultimately, the pain of using your data structure might indicate that there might be a better solution out there. That depends on your very specific needs, though, and you'll most likely discover that on iterating over your project.

WalternativE
  • 493
  • 4
  • 15
  • 1
    I thought exactly what you described, a non obvious difference between an inferred HOF and the generality of the module binded one. Sorry for not detailing this in the question I were really tired when asking. I also saw that the inferred type was dependant of the first case (which is logical). I've been wandering what you said, if it's the right design, probably I should create a new question about the specifics of the design and the decisions involved. The domain has a big amount of what will be easily achieved through dynamic typing and Metaprogramming, which are not F# strong points, but – Rodrigo Oliveri Dec 07 '19 at 14:53
  • 1
    but I thought I could come with a "right" way of doing it that doesn't involve box nor string parsing. Thanks for your answer. – Rodrigo Oliveri Dec 07 '19 at 14:54
  • Yeah, maybe set up a question specific to your design goal. You could also link to the question in this thread (maybe as an edit to your original question and provide a link back in the new one to provide more context). I'm currently not really sure how to evolve on my answer so a new more focused thread might be good. – WalternativE Dec 07 '19 at 18:22
2
let listGenericFunc (func: EspecificList -> 'a) (l: EspecificList) =
    match l with
    | IntList _ -> func l
    | BoolList _ -> func l

This seems to be just another pointless variation, since you might as well just call func directly with l. So it looks like we're back to @WalternativE's conclusion again.

Btw, it's not at all the first time somebody asks about generics and DUs combined in ways like this, and it looks like it just isn't meant to be. Maybe someone smarter than me can elaborate.

Bent Tranberg
  • 3,445
  • 26
  • 35