robkuz
robkuz

Reputation: 9934

How can I map over a record using interfaces

given the following types and value

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

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

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

I want to create a generic mapping function

tmap: X<'a, 'b> -> X<'x, 'y>

using interfaces and object expressions. My approach so far was

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

let inline tmap (f:ITransform<_,_,_,_>) ({y = yi; z = zi}) =
    {
        y = f.Apply yi
        z = f.Apply zi
    }

However I get an error at z = f.Apply zi as f is inferred to be ITransform<'a, int, 'b, int>

let mkStringify () =
    { 
        new ITransform<_,_,_,_> with 
            member __.Apply(Item(a,b)) = Item (sprintf "%A" a, b)
    }

let mkDublicate () =
    { 
        new ITransform<_,_,_,_> with 
            member __.Apply(Item(a,b)) = Item ((a, a), b)
    }

let x = tmap (mkStringify()) a
let y = tmap (mkDoublicate()) a

This is a follow up question to How to define a fmap on a record structure with F#.
I can get this solved by using the static member function approach described in one of the answers but not the interface approach

Upvotes: 2

Views: 99

Answers (1)

Fyodor Soikin
Fyodor Soikin

Reputation: 80915

Your ITransform definition is no better than a function. You could have just used a straight up function of signature Item<'a,'b> -> Item<'x,'y>, it would have worked the same.

The reason to use an interface is that you can have different generic parameters every time you call the method. But this, in turn, means that the generic parameters cannot be fixed on the interface itself. They must be on the method:

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

Or you can just drop them altogether, the compiler will lift them from the signature:

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

Now tmap compiles fine: even though the interface itself is not generic, its method Apply is, and so it can have different generic arguments on every call.

However, now you have another problem: implementing such interface in mkStringify is not that straightforward. Now that Apply is completely generic, its implementation cannot return specific types, such as string. You can't have your cake and eat it, too: the interface is a "promise" to the consumer and a "demand" on the implementer, so if your consumer expects to be able to do "anything", then the implementer must oblige and implement "everything".

To fix this, step back and think about your problem: what is it exactly that you want to achieve? What do you want to convert into what? So far it seems to me that you're trying to coerce the first argument of all Items to string, while keeping the second argument intact. If this is the goal, then the definition of ITransform is obvious:

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

This reflects the idea: the first argument of incoming Item may be anything, and it gets converted to a string, and the second argument may be anything, and it is left intact.

With this definition, both tmap and mkStringify will compile.

If this is not your goal, then please describe it, and we may be able to find another solution. But keep in mind the cake-related remark above: if you want tmap to work for any types whatsoever, then the implementers of ITransform must also support any types whatsoever.

Update

From discussion in the comments it became evident that the real problem description is as follows: the conversion function should convert the first argument of the Item to something else, and leave the second argument intact. And the "something else" would be the same for both Items.

With this, the implementation becomes clear: the interface itself should fix the "something else" part for the output, and the method should take any types as input:

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

With this definition, all three functions tmap, mkStringify, and mkDuplicate will compile. We have found a common ground: just enough promise to the interface consumer, and not too much demand on the interface implementer.

Having said that, I think that you don't really need an interface here, it's overkill. The reason you can't use a function is that a function will lose its genericity when passed by value, and so wouldn't be applicable to the different types of arguments. This, however, can be defeated by passing the function in twice. It will lose genericity in both instances, but it will lose it in different ways - i.e. it will be instantiated with different arguments every time. Yes, it feels awkward to pass the same function twice, but it's still less syntax than the interface:

let inline tmap f1 f2 ({y = yi; z = zi}) =
    {
        y = f1 yi
        z = f2 zi
    }

let stringify x =
    let f (Item(a,b)) = Item (sprintf "%A" a, b)
    tmap f f x

stringify a

Upvotes: 2

Related Questions