4
votes

I'm trying to write some numerical code that can work with either scalars or vectors (in this case it's the D and DV types respectively, from DiffSharp). Sometimes I want to be able to use either so I've defined a discriminated union for them:

type IBroadcastable =
    | Scalar of D
    | Vect of DV

A lot of operators are already overloaded for both of these types, so to use them on IBroadcastable I write add code like this to the union:

static member Exp x = 
    match x with
        | Scalar x -> Scalar (exp x)
        | Vect x -> Vect (exp x)

This seems very redundant. Is there any way I can use the operator on the union without having to write a new overload for it? Or I should I be using a different pattern (i.e. not a discriminated union)? An example of what I want to use this type for:

let ll (y: IBroadcastable) (theta: IBroadcastable) = y*theta-(exp theta)

The * and - will have more complicated behaviour (array broadcasting), which it makes sense to have to describe myself, but the exp operator is simple, as above. This needs to be a function since I want to be able to partially apply the y argument, get the gradient with DiffSharp, and maximise it with respect to the theta argument.

3
I don't see why you need the union at all. What does it give you compared with just using the operator on D or DV, whichever one you happen to have on hand at the moment? - Fyodor Soikin
@FyodorSoikin I'm trying to write a function where one argument could be either of the types, and it seemed like a union was the usual way to do that. Note: I'm a fairly new F# programmer, if that wasn't clear. - Christiaan Swanepoel
How would you use that function? And how would it be implemented? And why couldn't you use the implementation in place of the function? - Fyodor Soikin
I've realised the proper way to overload the exp operator is by adding a static member to the union so I'll updated the question. - Christiaan Swanepoel
@FyodorSoikin I've added an example. - Christiaan Swanepoel

3 Answers

2
votes

Fundamentally, since you're defining an abstraction, you need to define your operations in terms of that abstraction. That's a cost that has to be offset by the convenience it affords you elsewhere in your code.

What you may be wondering is if F# will let you cut on the boilerplate in your particular case. Apart from using the function keyword, not really, because both branches are really doing different things: the type of the bound variable x is different, and you're wrapping them in different union cases. If you were really doing the same thing, you could write it as such, for example:

type DU =
| A of float * float
| B of float * string
with
    static member Exp = function
        | A (b, _)
        | B (b, _) -> exp b // only write the logic once
2
votes

Your sample function ll is actually even more generic - it can work on anything that supports the operations it uses, even things that are not D or DV. If you define it using inline, then you will be able to call the function on both:

let inline ll y theta = y*theta-(exp theta)

The inline modifier lets F# use static member constraints, which can be satisfied by the required members when calling the function (unlike with normal generic functions that have to be compiled using what .NET runtime provides).

I expect this will not work for all your code, because you will need some operations that are specific to D and DV, but do not have generic F# function such as exp. You can actually access those using static member constraints, though this gets a bit hairy.

Assuming D and DV values both have a member Foo returning string, you can write:

let inline foo (x:^T) = 
  (^T : (member Foo : string) x)

let inline ll y theta = y*theta-(exp theta)+foo y
1
votes

You can cut down on the boilerplate by doing something like this:

type IBroadcastable =
| Scalar of D
| Vect of DV

let inline private lift s v = function
| Scalar d -> Scalar (s d)
| Vect dv -> Vect (v dv)

type IBroadcastable with
    static member Exp b = lift exp exp b
    static member Cos b = lift cos cos b
    ...

and if you want to support binary operators, you can define a corresponding lift2 - but carefully consider whether it makes sense for the first argument to a binary operator to be a Scalar value and the second to be a Vect (or vice versa) - if not, then your discriminated union might not be an appropriate abstraction.