Standard name of a sum type like Either but for 3 cases? [closed]
Asked Answered
F

6

10

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

Fatuous answered 7/8, 2016 at 12:14 Comment(7)
Not very commonly used: hackage.haskell.org/package/oneOfN-0.1.0.1/docs/…Selfinduced
I'm fond of the name ThreeitherStephanstephana
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.Wristwatch
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.0Dendro
@Dendro that's right, all I asked is a name for itBanded
@Wristwatch 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 caseBanded
Either () (Either () ()) is a type with exactly 3 distinct values: Left (), Right (Left ()), and Right (Right ()).Keratose
H
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.)

Henry answered 7/8, 2016 at 16:32 Comment(2)
"You get a value or an exception." I disagree. 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
Consider the ways 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
U
9

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.

Ure answered 7/8, 2016 at 16:11 Comment(1)
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.Koan
P
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.

Phosphorylase answered 7/8, 2016 at 21:54 Comment(0)
T
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).

Tamis answered 7/8, 2016 at 12:34 Comment(1)
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 haveBanded
B
3

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

Borras answered 7/8, 2016 at 14:33 Comment(0)
T
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")))
Tieback answered 13/12, 2022 at 22:49 Comment(1)
This is nice when you can encapsulate all the processing for each type. If you can't for some reason, it can be useful to use 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.