10

Is there a standard sum type like Either but for 3 cases? Haskell has These but it's not quite that.

Sehnsucht
  • 5,019
  • 17
  • 27
Trident D'Gao
  • 18,973
  • 19
  • 95
  • 159
  • 2
    Not very commonly used: http://hackage.haskell.org/package/oneOfN-0.1.0.1/docs/Data-OneOfN.html – chi Aug 07 '16 at 12:18
  • I'm fond of the name `Threeither` – Benjamin Hodgson Aug 07 '16 at 12:40
  • 5
    What do you want this for? Even `Either` is often a bit vague; once you need three constructors you're almost certainly better of defining a new type whose name and constructor names describe what they're supposed to mean in context. – dfeuer Aug 07 '16 at 15:47
  • 1
    Providing you want a sum type of non primitives, the idiomatic typescript code would be something like: type MyChoice = { name: 'choice1' } | { name: 'choice2' } | { name: 'choice3' } Where each sum type can have its own data. You can then type guard efficiently when using TS 2.0 – AlexG Aug 07 '16 at 17:03
  • 1
    @AlexG that's right, all I asked is a name for it – Trident D'Gao Aug 07 '16 at 17:54
  • 1
    @dfeuer because i need to return one of 3 possible things out of a function and i think that a dedicated data type is an overkill for this single use case – Trident D'Gao Aug 07 '16 at 17:55
  • `Either () (Either () ())` is a type with exactly 3 distinct values: `Left ()`, `Right (Left ())`, and `Right (Right ())`. – chepner Aug 08 '16 at 02:33

6 Answers6

18

I think that heavily relying on type like this is an anti-pattern.

One of the nicest things you get from using algebraic data types is that the resulting type tells you something about the domain that you are working with. With a generic type like Choice<T1, T2, T3>, you are really not saying anything about the domain.

I think option<T> (aka Maybe) is quite clear in that it says that a value of type T is either there or it is missing for some reason. I think Either<'T, exn> is still quite clear in that it says you get a value or an exception. However when you have more than two cases, it becomes quite hard to understand what is meant by the cases and so explicitly defining a type with names to match the domain might be a good idea.

(I do use Choice<T1, T2, T3> in F# occasionally, but the usage is typically limited to a small scope - less than 50 lines of code - so that I can easily find what the meaning is in the surroundings of the code that consumes it.)

Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
9

In recent Haskell, I'd switch on a bit of kitchen sink.

{-# LANGUAGE PolyKinds, DataKinds, GADTs, KindSignatures,
    TypeOperators, PatternSynonyms #-}

Then I'd define type-level list membership

data (:>) :: [x] -> x -> * where
  Ze ::            (x ': xs) :> x
  Su :: xs :> x -> (y ': xs) :> x

and now I have all the finite sums, without cranking out a whole raft of OneOfN type definitions:

data Sum :: [*] -> * where
  (:-) :: xs :> x -> x -> Sum xs

But, to address Tomas's issue about readability, I'd make use of pattern synonyms. Indeed, this sort of thing is the reason I've been banging on about pattern synonyms for years.

You can have a funny version of Maybe:

type MAYBE x = Sum '[(), x]

pattern NOTHING :: MAYBE x
pattern NOTHING = Ze :- ()

pattern JUST :: x -> MAYBE x
pattern JUST x = Su Ze :- x

and you can even use newtype to build recursive sums.

newtype Tm x = Tm (Sum '[x, (Tm x, Tm x), Tm (Maybe x)])

pattern VAR :: x -> Tm x
pattern VAR x = Tm (Ze :- x)

pattern APP :: Tm x -> Tm x -> Tm x
pattern APP f s = Tm (Su Ze :- (f, s))

pattern LAM :: Tm (Maybe x) -> Tm x
pattern LAM b = Tm (Su (Su Ze) :- b)

The newtype wrapper also lets you make instance declaration for types built that way.

You can, of course, also use pattern synonyms to hide an iterated Either nicely.

This technique is not exclusive to sums: you can do it for products, too, and that's pretty much what happens in de Vries and Löh's Generics-SOP library.

The big win from such an encoding is that the description of data is itself (type-level) data, allowing you to cook up lots of deriving-style functionality without hacking the compiler.

In the future (if I have my way), all datatypes will be defined, not declared, with datatype descriptions made of data specifiying both the algebraic structure (allowing generic equipment to be computed) of the data and its appearance (so you can see what you're doing when working with a specific type).

But the future is sort of here already.

Cactus
  • 27,075
  • 9
  • 69
  • 149
pigworker
  • 43,025
  • 18
  • 121
  • 214
8

These are called co-products really an Either is simply a 2 argument co-product. You can use helpers from the shapeless library to build arbitrary length co-products using:

type CP = Int :+: String :+: Boolean :+: CNil

val example = Coproduct[CP]("foo")

You can then use all the fun poly magic to map them or perform other operations:

object printer extends Poly1 {
  implicit def caseInt = at[Int](i => i -> s"$i is an int")
  implicit def caseString = at[String](s => s -> s"$s is a string")
  implicit def caseBoolean = at[Boolean](b => s -> s"$b is a bool")
}
val mapped = example map printer
mapped.select[(String, String)] shouldEqual "foo is a string"

Scala.JS + Shapeless can work together as far as I know, so that may give you what you want.

flavian
  • 28,161
  • 11
  • 65
  • 105
  • This appears to be the only answer that actually answers the question. I thought I was on Quora for a second there! Anyways, thank you. – Mattias Martens Apr 06 '21 at 16:24
3

Which language are you using? If it's F#, there's a three-way Choice<'T1,'T2,'T3> type. (Also a 4-, 5-, 6- and 7-way Choice type in addition to the more "standard" two-way type).

rmunn
  • 34,942
  • 10
  • 74
  • 105
  • i am using TypeScript (which doesn't have a standard lib other than that of JavaScript which lacks types), so I am getting inspired by what other languages have – Trident D'Gao Aug 07 '16 at 12:45
3

For scala there's the Either3 from Scalaz: https://github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/scalaz/Either3.scala

gregghz
  • 3,925
  • 7
  • 40
  • 69
0

Copying the second half of my answer from another question: accept multiple types for a parameter in scala.

With a few changes, here's a solution when we have to accept multiple Types:

def doSomething[C,T](obj: C): T = {
  obj match {
    case objA: ClassA => processA(objA.fieldA)
    case objB: ClassB => processB(objB.fieldB)
    case objC: ClassC => processC(objC.fieldC)
  }
}

doSomething[InputTypeA, ReturnTypeA](new ClassA(fieldA=InputTypeA("something")))

doSomething[InputTypeB, ReturnTypeB](new ClassB(fieldB=InputTypeB("somethingese")))

doSomething[InputTypeC, ReturnTypeC](new ClassC(fieldC=InputTypeC("another")))