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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With