30

In many years of OO programming I've understood what discriminated unions are, but I never really missed them. I've recently been doing some functional programming in C# and now I find I keep wishing I had them. This is baffling me because on the face of it, the concept of discriminated unions seems quite independent of the functional/OO dichotomy.

Is there something inherent in functional programming that makes discriminated unions more useful than they would be in OO, or is it that by forcing myself to analyse the problem in a "better" way, I've simply upped my standards and now demand a better model?

  • The expression problem en.wikipedia.org/wiki/Expression_problem might be relevant – xji Jul 12 '17 at 15:50
  • It is not really a proper response to the question, so I will reply as a comment instead, but Ceylon programming language has a union types and they are seemingly creeping in to other OO/mixed paradigm languages — TypeScript and Scala come to my mind. Also Java language enums can be used as a sort of implementation of discriminated unions. – Roland Tepp Jul 13 '17 at 9:34
45

Discriminated unions really shines in conjunction with pattern-matching, where you select different behavior depending on the cases. But this pattern is fundamentally antithetical to pure OO principles.

In pure OO, differences in behavior should be defined by the types (objects) themselves and encapsulated. So the equivalence to pattern matching would be to call a single method on the object itself, which is then overloaded by the sub-types in question to define different behavior. Inspecting the type of an object from the outside (which is what pattern matching does) is considered an antipattern.

The fundamental difference is that data and behavior is separate in functional programming, while data and behavior are encapsulated together in OO.

This is the historical reason. A language like C# is developing from a classic OO language to multi-paradigm language by incorporating more and more function features.

  • 6
    A discriminated union/sum type isn't like an inheritance tree or multiple classes implementing an interface; it's one type with many kinds of values. They also don't prevent encapsulation; the client doesn't need to know you have multiple kinds of values. Consider a linked list, which may be either an empty node, or a value and a reference to another node. Without sum types, you end up hacking together a discriminated union anyways by having variables for the value and reference, but treating an object with a null reference as an empty node and pretending the value variable doesn't exist. – Doval Jul 7 '17 at 14:57
  • 2
    In general, you end up including the variables for all the possible kinds of values in one class, plus some sort of flag or enum to tell you what kind of value that instance is, and dancing around the variables that don't correspond to that kind. – Doval Jul 7 '17 at 15:00
  • 7
    @Doval There's a "standard" encoding of an algebraic data type into classes by having an interface/abstract base class represent the type overall and by having a subclass for each case of the type. To handle pattern matching, you have a method that takes a function for each case, so for a list you'd have on the top-level interface, List<A> the method B Match<B>(B nil, Func<A,List<A>,B> cons). For example, this is exactly the pattern Smalltalk uses for booleans. This is also basically how Scala handles it. That multiple classes are used is an implementation detail that need not be exposed. – Derek Elkins Jul 7 '17 at 17:19
  • 3
    @Doval: Explicitly checking for null references is not really considered idiomatic OO either. – JacquesB Jul 7 '17 at 17:57
  • @JacquesB The null check is an implementation detail. For the client of the linked list there's usually an isEmpty method that checks if the reference to the next node is null. – Doval Jul 7 '17 at 23:53
34

Having programmed in Pascal and Ada before learning functional programming, I don't associate discriminated unions with functional programming.

Discriminated unions are in some way the dual of inheritance. The first allow to easily add operations on a fixed set of types (those in the union), and inheritance allows to easily add types with a fixed set of operations. (How to to easily add both is called the expression problem; that is an especially hard problem for languages with a static type system.)

Due to the emphasis of OO on types, and the dual emphasis of functional programming on functions, functional programming languages have a natural affinity for union types and offer syntactic structures to ease their use.

7

Imperative programming techniques, as often used in OO, rely often on two patterns:

  1. Succeed or throw exception,
  2. Return null to indicate "no value" or failure.

The functional paradigm typically avoids both of these, preferring to return a compound type that indicates success/failure reason or value/no value.

Discriminated unions fit the bill for these compound types. For example, in the first instance, you might return true, or some data structure that describes the failure. In the second case, a union that contains a value, or none, nil etc. The second case is so common, that many functional languages have a "maybe" or "option" type built-in to represent that value/none union.

When switching to a functional style with eg, C#, you'll quickly find a need for these compound types. void/throw and null just don't feel right with such code. And discriminated unions (DUs) fit the bill well. Thus you found yourself wanting them, just like lots of us have.

The good news is that there are plenty of libraries out there that model DUs in eg C# (have a look at my own Succinc<T> library for example).

2

Sum types would be generally less useful in the mainstream OO languages as they are solving a similar type of problem to OO subtyping. One way to look at them is that they both handle subtyping but OO is open i.e. one can add arbitrary subtypes to a parent type and sum types are closed i.e. one determines upfront what subtypes are valid.

Now, many OO languages combine subtyping with other concepts such as inherited structs, polymorphism, reference typing etc to make them generally more useful. A consequence is that they tend to be more work to set up (with classes and constructors and whatnot) so tended not to be used for things like Results and Options and so on until generic typing became common.

I'd also say that the focus on real-world relationships that most people learnt when they began OO programming e.g. Dog isa Animal, meant that Integer isa Result or Error isa Result seem a bit alien. Though the ideas are quite similar.

As to why functional languages might prefer closed typing to open typing, one possible reason is that they tend to prefer pattern matching. This is useful for function polymorphism but it also works really well with closed types as the compiler can statically check that the matching is covering all subtypes. This can make the language feel more consistent though I don't believe there's any inherent benefit (I could be mistaken).

  • BTW: Scala has closed inheritance. sealed means "can only be extended in the same compilation unit", which allows you to close the set of subclasses at design time. – Jörg W Mittag Jul 8 '17 at 23:50
-3

Swift happily uses discriminate unions, except it calls them "enums". Enums are one of the five fundamental categories of objects in Swift, which are class, struct, enum, tuple, and closure. Swift optionals are enums which are discriminate unions, and they are absolutely essential for any Swift code.

So the premise that "discriminate unions are associated with functional programming" is wrong.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.