Algebraic Data Types in Haskell
serokell.io
serokell.io
I often try to replicate ADTs in Python at $DAYJOB, because they're so damn convenient. I end up with dataclasses which have the same interface, and I disambiguate between them using `isinstance`. It's not perfect but it's useable.
I also do this! It is so painful not to have real ADTs.
While it doesn't bring you Haskell ADTs, it sounds like it could make your `isinstance`-style code a lot cleaner.
Data Modeling Made Functional
https://www.amazon.com/Domain-Modeling-Made-Functional-Domai...
It's one of the better and more useful software engineering books I've read. Even if you don't use a functional programming langauge. It's about using Algebraic Data Types do model common problems in the day-to-day business domain (not typical academic problems).
It's a really simple and awesome presentation, and by the end you're dying for the ability to use this more so in the day to day job. Honestly after reading through it, trying to model problems in OOP just seems so unnecessarily obtuse.
The Scott Wlaschin also runs https://fsharpforfunandprofit.com
https://fsharpforfunandprofit.com/ddd/ - a link to a talk on it which is decent, but the book is much better.
Tuples can be emulated using structs but it can generate a lot of boilerplate for a single function. The only alternative to express iterating over "zipped" lists is to have the two lists side by side and iterate using an integer.
However, sum types are just plain missing. I guess interfaces help, but they're really limiting in what they allow and even regular C-style enums are painful and can't be checked for exhaustivity at compile time.
Does anyone have tips on what the "idiomatic" solutions are for these problems?
This is true of OO languages in general, not just Go.
I am familiar with Haskell. I know the gospel of sum types. But I don't think it's good engineering to force an inferior solution in to solve a problem I don't have.
There are times when it solves problems. It is sort of ironic that as this conversation was occurring here on HN, I was programming with a sum type at work and doing a lot of type switches today. But it was solving a problem for me. Either/Result doesn't solve a problem I have in Go.
The vast majority of time a person bashes a sum type into Go, they should be using polymorphism instead of switches.
It is a well-known error to try to use a functional programming language as an OO language. It is the exact same error to use an OO language as if it's a functional language. One may be more in line with the zeitgeist, but that just makes it more popular, not a better idea. It's just as silly and just as gauche as the guy who runs to a Haskell community to complain about how they can't figure out how to implement inheritance in a nice way.
You should look into functools.singledispatch if you're doing business logic with isinstance. But you should also learn about object-oriented programming, inheritance vs composition, mixins, etc. People get sour on OOP because they should have learned FP first and then imitate/translate FP idioms -- instead of doing something like modeling domain ontologies in object graphs. Likewise, people get sour on FP yadda yadda instead of trying to literally write their programs as proofs to theorems.
It’ll get rid of the ‘isinstance’ and give exhaustive matching. I used to use this pattern a lot when wanting ADT’s in a language without ADT’’s.
[1] https://yourlabs.io/pyratzlabs/pymich/-/blob/master/pymich/m...
See struct visitors in https://github.com/llvm/llvm-project/blob/main/flang/include...
class Point { public int x; public int y; }
...but it can't say a `Point` has _either_ `x` and `y` OR `r` and `theta`. You can simulate it with two classes and a base class/interface but that implies there might be _other_ ways of defining a point... which there really aren't. The Haskell point would be: data Point = Cartesian { x :: Double, y :: Double } |
Polar { r :: Double, theta :: Double } sealed interface Point {}
record Cartesian(double x, double y) implements Point {}
record Polar(double r, double theta) implements Point {}[1]: https://openjdk.java.net/jeps/420
const MyNumberUnion = union { small: u8, medium: u16, large: u32, }
The values of MyNumberUnion can have a small, medium, OR large, but only one of those.
Despite some downside, I'm a huge fan overall though. I haven't tried ReasonML or ReScript, but compared to bare JS, TypeScript makes frontend programming a lot more enjoyable to me.
But I think what was meant is that typescript's mostly committed to not changing runtime behavior of the JS that remains when you strip the types away, and while I we might disagree with that goal or with its application in this instance, I can at least see the flow of the reasoning and label it a matter of priorities.
Abuse Of Some Sum Types In OO Languages http://www.jerf.org/iri/post/2960
If we were to believe that "build yet another interface every time" was an equally good solution which is just a different flavour from sum types we'd expect to see equivalent APIs in Go with such interfaces and we do not. It's not a different style, it's just worse.
Example, suppose I got a string from a user, u, and I am now trying to see where that string is in another string I have from somewhere, x
In Rust x.find(u) returns Option<usize>, ie either None or Some(number)
But in Go strings.Index(x, u) returns int, and I have to know that the integer -1 is used as a sentinel value meaning "not found" rather than "found at position -1".
OK, so strings don't have great ergonomics, how about file opening? Let's open a file named george.txt which alas might not exist.
In Rust File::open("george.txt") returns Result<File>, ie either an Error or Ok(File)
But in Go os.Open("george.txt") return a tuple with both an error and my file and then I need to check whether the error was actually nil (no error).
For example, if you're designing an API in Go, you should follow the established convention for error handling, rather than come up with your own weird thing, unless there's something special going on. Go's error handling is verbose but it's well understood and it's not broken.
Extensibility in OO languages can be great for many things, but ADTs excel at expressing a fixed set of data shapes, forcing programmers to think carefully and break things down into a fixed set of cases. When you've handled all the cases, you're done!
But in the end, it’s just fancy words for union and intersection types, or am I missing something ?
(The "sum" and "product" are why they're called "algebraic". You can also analyze them as power series, such as in computing derivatives for zippers)
Intersection types are something else (that Haskell does not have built in to my knowledge).
"union" is slightly imprecise, because sum types are tagged (i.e., a non-discriminated union of Int and Int is just Int, but Int+Int is actually the same as (Bool, Int). True non-discriminated union types are relatively uncommon in static type systems, but TypeScript for example does have them)
> I wonder if there is an interesting interaction between those.
There's a rather rich theory of polynomial endofunctors (i.e. generic types with one type parameter built from products, sums, and non-generic types), and I think monads figure prominently (they're somehow related to the initial algebras for such functors). So there's definitely a relationship there.
The dynamic scripting languages can do a "maybe" type because they can do just about anything when it comes to types; what they can't do is enforce correct usage by restrictions, so you write a Maybe type but you can't ever quite be sure it's doing what you think it's doing. This is just something you end up living with in dynamically typed langauges.
For instance you can certainly do:
type Maybe<X> = { tag: 'just', value: X } | { tag: 'nothing'};
type Either<X, Y> = { tag: 'left', value: X } | { tag: 'right', value: Y };
function either<X, Y, Z>(fromLeft: (x: X) => Z, fromRight: (y: Y) => Z, xy: Either<X, Y>): Z {
switch (xy.tag) {
case 'left': return fromLeft(xy.value);
case 'right': return fromRight(xy.value);
}
}
Note that this is meaningfully different from the (often more idiomatic and certainly less verbose) untagged use of union types because you can distinguish left from right even when the contained types agree and you can distinguish different nestings, for better and for worse. data Bool = False | True
data Maybe a = Nothing | Just a
Bool is a nullary type constructor or simply type. False and True are nullary data constructors or simply constants. Maybe is an unary type constructor taking one parameter to construct a fully saturated type (eg. Maybe Int), but calling it a "Maybe type" is ok, no crazy ambiguity. Nothing is a constant and Just is an unary data constructor taking one parameter to construct fully saturated data (eg. Just 1).Bool = sum(False, True)
Maybe = λ a . sum(Nothing, Just(a))
Type constructors, yes. In Haskell the type of type constructors are called kinds:
λ> :kind Bool
Bool :: Type
λ> :kind Maybe
Maybe :: Type -> Type
λ> :kind Maybe Int
Maybe Int :: Type
λ> :kind Either
Either :: Type -> Type -> Type
λ> :kind Eq
Eq :: Type -> Constraint
λ> :kind Functor
Functor :: (Type -> Type) -> ConstraintOn the other hand, `Just` and `Nothing` are constructors, or variants, for values of type `Maybe a`. You can think of them as almost like static factories (as in `Optional.empty()` and `Optional.of(val)` in Java), except that algebraic constructors are invertible: you can match on a value to determine whether it was constructed with `Just` or `Nothing`, and what the arguments to the constructor were.