Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Purpose of a single case discriminated union

I'm defining a monadic observable/reactive parser. This behaves quite differently to a normal parser as it is a continuous query. The underlying type is:

IObservable<'a> -> IObservable<'b>

From looking at various parser implementations in functional languages, it seems as though the more appropriate way to define things is a single case discriminated union:

type Pattern<'a,'b> = Pattern of (IObservable<'a> -> IObservable<'b>)

Which means I then need to extract the underlying function to use it:

let find (Pattern p) = p

The question is: Is this just by convention, or for purposes of later extension, or is there a reason to do this even if the definition never changes?

Bonus question: If it's just for a more convenient type signature, why not just use a type alias:

type Pattern<'a,'b> = IObservable<'a> -> IObservable<'b>

I've advanced quite a way through this, and haven't found a case where composability is affected by not using the DU.

like image 585
yamen Avatar asked Apr 14 '12 03:04

yamen


People also ask

When to use discriminated union?

Discriminated unions are useful for heterogeneous data; data that can have special cases, including valid and error cases; data that varies in type from one instance to another; and as an alternative for small object hierarchies.

What are discriminated union?

A discriminated union is a union data structure that holds various objects, with one of the objects identified directly by a discriminant. The discriminant is the first item to be serialized or deserialized. A discriminated union includes both a discriminant and a component.

What is a union in F#?

In F#, a sum type is called a “discriminated union” type. Each component type (called a union case) must be tagged with a label (called a case identifier or tag) so that they can be told apart (“discriminated”). The labels can be any identifier you like, but must start with an uppercase letter.


1 Answers

F# compiler doesn't preserve information about type abbreviation, so you do not benefit from type inference at all. Type signature can be understood as program specification; letting the type checker do their job is a good way to ensure correctness of your programs.

You need explicitly specify type annotation everywhere in the case of type alias:

type Vector = float * float

// val add : float * float -> float * float -> Vector
let add ((x1, y1): Vector) ((x2, y2): Vector): Vector = (x1 + y1, x2 + y2)

but it doesn't give you transparency as using DUs:

type Vector = V of float * float

// val add : Vector -> Vector -> Vector
let add (V(x1, y1)) (V(x2, y2)) = V(x1 + y1, x2 + y2)

In complex programs clear type signatures indeed make it easier to maintain composability.

Not only it's simpler to add more cases to single-case DUs, but also it's easier to extend DUs with member and static methods. One example is you often override ToString() for pretty printing.

like image 191
pad Avatar answered Sep 20 '22 05:09

pad