Is there a standard sum type like Either but for 3 cases? Haskell has These
but it's not quite that.
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.)
Either
just says you get OneOf 2 different things and has convenient names (left and right) for them. The Left
is only an exception by convention because Either assumes/defaults to Right
for the type system, so the Left
can't be very useful since it's type is treated as second class in collection mappings etc. –
Chamomile partitionMap
is safer than collect
(for handling "both" subtypes exactly once). Then, it becomes obvious how you don't always use Either for exceptions and in fact, might really enjoy a few more types in there besides 2. All the functionality is there in the compiler with pattern matching and partitionMap
to handle a sum type of 3 or more, but ironically, you have to chain partitionMap
multiple times to decompose what pattern matching can do in one pass. –
Chamomile 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.
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.
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).
For scala there's the Either3
from Scalaz: https://github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/scalaz/Either3.scala
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")))
partitionMap
to break them into separate collections first and then process them at your leisure. But, unfortunately, you have to apply partitionMap
for as many additional types as you have. –
Chamomile © 2022 - 2024 — McMap. All rights reserved.
Threeither
– StephanstephanaEither
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. – WristwatchEither () (Either () ())
is a type with exactly 3 distinct values:Left ()
,Right (Left ())
, andRight (Right ())
. – Keratose