7

is there a possibility to create an fmap for records so that I can apply the same function to record fields of similar bur different types

Let say I have a record field type Item and a record X and function transform

type Item<'a, 'b> = Item of 'a * 'b

let transform (i: Item<'a, 'b>) : Item<'a, string> = 
    let (Item (x, y)) = i
    Item (x, sprintf "%A" y)

type X<'a> = {
    y: Item<'a, int>
    z: Item<'a, bool>
}
with
    member inline this.fmap(f) =
        {
            y = f this.y
            z = f this.z
        }

now the line z = f this.z complains that the given type should be of Item<'a, int> but its of type Item<'a, bool>. Obviously as the type infererrer
has decided that the function f is of type Item<'a, int> -> Item<...> however i want f to be applied polymorphic. How can I get this done?

Evil type hacks are welcome!

Guy Coder
  • 24,501
  • 8
  • 71
  • 136
robkuz
  • 9,488
  • 5
  • 29
  • 50
  • 1
    Function genericity only works at the definition. It is lost once you make it a "value" and start passing it around. If you need to keep the genericity, use an interface. – Fyodor Soikin Dec 13 '16 at 14:36
  • So f should be any function 'a->'a where 'a could be a string or a bool? There are not many functions I can think apart from ``id``. – Gus Dec 13 '16 at 14:48

2 Answers2

2

An obvious solution is to use bimap instead of fmap and then write twice the function at the caller site:

type Item<'a, 'b> = Item of 'a * 'b

let transform (i: Item<'a, 'b>) : Item<'a, string> = 
    let (Item (x, y)) = i
    Item (x, sprintf "%A" y)

type X<'a> = {
    y: Item<'a, int>
    z: Item<'a, bool>
}

with
    member inline this.bimap(f, g) =
        {
            y = f this.y
            z = g this.z
        }

Another alternative (here's the evil type hack) is instead of passing a function, pass what I call an 'Invokable' which some kind of function wrapped in a type with a single method called Invoke. Something like a delegate but static.

Here's an example. I use $ instead of Invoke for simplicity:

let inline fmap invokable ({y = y1; z = z1}) = {y = invokable $ y1; z = invokable $ z1}


type Id = Id with 
    static member ($) (Id, Item (a,b)) = Item (id a, id b)

type Default = Default with 
    static member ($) (Default, Item (a:'t,b:'u)) = 
        Item (Unchecked.defaultof<'t>, Unchecked.defaultof<'u>)

let a = {y = Item ('1', 2); z = Item ('3', true) }

let b = fmap Id a
let c = fmap Default a

Now the problem is I can't think of many other useful functions. Can you?

Otherwise if you make it more generic:

type X<'a, 'b, 'c> = {
    y: Item<'a, 'b>
    z: Item<'a, 'c>
}

then you can for instance use an Invokable like this:

type ToList = ToList with static member ($) (ToList, Item (a,b)) = Item ([a], [b])

let d = fmap ToList a
// val d : X<char list,int list,bool list> = {y = Item (['1'],[2]);
                                       z = Item (['3'],[true]);}

See also this related question. The case presented there is simpler but the problem is the same.

Also this one is related.

Gus
  • 25,839
  • 2
  • 51
  • 76
  • BTW I don't know if you already noticed but most polymorphic functions in FsControl are Invokables ;) – Gus Dec 13 '16 at 15:15
  • First thanks to Gustavo Fyodor and @kvb - I think it really clicked this time for me. So the **solution** is to create some polymorphic delegate type instead of my func and some associated method and call this method when invoking/applying where gustavo 's proposal being a bit more condensed. However - dont you all think that this is all a bit very heavy weight? I mean pushing a polymorphic function as a first class function parameter isnt really something particular esoteric! why does the type inferrer interfere in such cases? Just a rhetoric question ;-) – robkuz Dec 13 '16 at 16:14
  • @robkuz is not just F# type inference. I guess all languages having Hindly-miner like type inference behaves the same regarding this. Even Haskell, but if you read the first link I sent you it seems that F# is more flexible than Haskell in these cases ;) – Gus Dec 13 '16 at 16:40
  • I am just wondering if a feature akin to type classes would easen the pain? – robkuz Dec 13 '16 at 21:10
  • I don't think so. As I said, even in Haskell you'll have this limitation. This has more to do with higher ranks and in Haskell you can use them but in this scenario they are more limiting than static constraints. – Gus Dec 14 '16 at 05:30
  • is there a way to combine this approach with fsControl? for example the signature of Map is `static member Map(SomeType<'a>, fn)`. How would I change this to so that I can exchange the `fn` with an `Invokable`? – robkuz Dec 16 '16 at 07:40
  • @robkuz No, you can't. You need to define a specific ``map`` that takes an Invokable and maps over the 'shape' you're dealing with. But you can use the ``Invoke.Invoke`` method and eventually reuse the generic functions from there, the problem is that I can't think of a good example with your code which is very restricted. But I can point you to the [trivial tuple example](https://github.com/gmpl/FsControl/blob/5ae4779050528b6f1183fb4f0447360664c53c0b/FsControl.Core/Samples/Haskell.fsx#L493-L499). I'm curious about what's the problem behind this you're trying to solve. Could you share a gist? – Gus Dec 16 '16 at 19:04
2

I agree with @Fyodor that using an interface is the cleanest solution if you need to express a polymorphic argument:

type Item<'a, 'b> = Item of 'a * 'b

let transform (i: Item<'a, 'b>) : Item<'a, string> = 
    let (Item (x, y)) = i
    Item (x, sprintf "%A" y)

type ITransform<'a,'x> = abstract Apply : Item<'a,'b> -> Item<'x,'b>

type X<'a> = {
    y: Item<'a, int>
    z: Item<'a, bool>
}
with
    member inline this.fmap(f:ITransform<_,_>) =
        {
            y = f.Apply this.y
            z = f.Apply this.z
        }
{ y = Item(1,2); z = Item(3,true) }.fmap 
    { new ITransform<_,_> with member __.Apply(Item(i,x)) = Item(i+1, x) }
Community
  • 1
  • 1
kvb
  • 54,864
  • 2
  • 91
  • 133
  • I have a follow up question on this here https://stackoverflow.com/questions/45088899/how-can-i-map-over-a-record-using-interfaces – robkuz Jul 13 '17 at 18:54