Hacker News new | ask | show | jobs
by hepta 3155 days ago
I do agree that there are problems where the "maps of facts" approach is better suited than ADTs. I'm not sure if those are the majority of problems we're solving. You can still do all that in a typed language, using dictionaries. You can be as precise as you want. In some sense you lose that without a type-checker.

I only have my experience (which is about 1/4 of his) to lean on. And I've only had problems when protocols are not well understood by all parties, never the opposite. Talking in the level of ADTs (or some representation that is equally precise) tends to either clear things up, or uncover problems that were not obvious in informal discussion.

2 comments

> Talking in the level of ADTs (or some representation that is equally precise)

I will argue that ADTs offer no more precision than first-class properties and first-class properties are much more aligned with human beings natural way of thinking.

I.e., "name" is semantically strong/precise, I don't need to enslave it to a Person class. I can give a Cat a name, a building a name, etc.

ADTs are trying to bundle properties together when those properties should/could have strong enough semantics on their own and be much more "composable" (RH's term) and freer to use.

Maybe we have to dig into the specifics: there's a lot of things that can be done with Name, regardless of where the name belongs, is that what you mean? If so, most typed languages let you say that Person can be a thing with a Name. I don't have Clojure experience so we might be talking past each other here.
What resonates most with me is that traditionally ADTs make the properties second-class citizen to the ADT/class.

First-class properties are strong enough semantics and bundling them together in an ADT is almost always introducing coupling and rigidity with no real benefit.

Why do we need the Person class? Why do we need to taxonomize like that? The answer is we don't. All the Person class brings to the table is straight-jacketing and a false sense of order and security.

I haven't watched the full video, but here are some things that a hypothetical Person class brings to the table that you might be missing:

1) A Person class can guarantee that a Person can not exist unless it is fully specified. E.g. you prevent Persons from being created that don't have a date of birth.

2) A Person class provides a syntactically nice place to put pieces of code that rely on the instance fields. E.g. a function like Person(...).computeAge(DateTime.now)

3) A Person class can be a nice place to put inter-field validations. E.g. Maybe you don't want to allow Persons to be constructed if their last name is the same as their first name

4) A Person class can bring clarity to function signatures that depend on it. Basically resolving the problems that Duck-typing introduces

What's a "first-class property"?

   class Person {
      def age;
   }
There is a second-class "age" property: the only way to specify it or use it is to first declare the ADT/Person class. (In this example, I use a dynamic language; that is, the property can be given any type of value.)

Here is the usage of a first-class age property:

   public boolean isOld(Map<String, Object> entity) {
      return entity.getAs("age", int.class) > 30;
   }
Note that in this example, age's semantics are independent of any Person/ADT. Now a statically-typed language that would let me define a vocabulary that include "age" and its type independent of an ADT would be supporting first-class properties.

I could cheat and do this in a typed language like Java like so:

   class Age { int val; } // using class to define a property
   class Entity { Age age; ...and list all other properties here... }
But this is absurdly cumbersome and obviously Java isn't conducive to this.
> Note that in this example, age's semantics are independent of any Person/ADT. Now a statically-typed language that would let me define a vocabulary that include "age" and its type independent of an ADT would be supporting first-class properties.

What you're referring to is known as row polymorphism or structural typing and it exists in languages such as Elm, Purescript, Scala and Go (and I believe Haskell has these as an extension). For instance, this is valid Scala:

    def isOld(thing: {def age: Int}): Boolean =
      thing.age > 30

    case class Person(name: String, age: Int)
    case class Dog(age: Int)
    println(isOld(Person("Jim", 25))
    println(isOld(Dog(31))
And here's Elm:

    isOld : {age: Int | a} -> Bool
    isOld thing = thing.age > 30

    Debug.log (isOld {name: "Jim", age: 42})
    Debug.log (isOld {age: 10})
This is pretty cool. The Scala version is a little weird. Why do I need a separate Person and Dog class? Can't a dog have a name as well? Why do I need to declare that upfront or ensure that no other part of code can give a Dog a name? That's the problem with ADTs; there's all this impedance.

The Elm version is much better. Except of course I have to upfront at the method signature declare my property requirements -- while that is also a burden, it is a much lesser burden than having ADT/taxonomy of Person, Dog, etc.

I think the Scala version does exactly what you want it to do, but you seem to be confused about something that I can't really pin-point.

> Why do I need a separate Person and Dog class?

You don't. It's an example.

> Can't a dog have a name as well?

It can.

> Why do I need to declare that upfront or ensure that no other part of code can give a Dog a name?

I have trouble understanding the meaning of that sentence.

> The Elm version is much better. Except of course I have to upfront at the method signature declare my property requirements -- while that is also a burden, it is a much lesser burden than having ADT/taxonomy of Person, Dog, etc.

Elm (and Purescript) should be able to fully infer the type of that function w/o forcing you to declare which fields you want to use up-front.

> I believe Haskell has these as an extension

Not really ... Haskell has libraries that claim to implement this but we Haskellers have to admit Haskell's support for row types is pretty poor. A Clojurist's objections to Haskell on those grounds are valid (in practice though not in theory).

Does this meet your criteria of "first-class" property? It works on any entity that "has" and "age", at least according to my definition of those things!

    -- The class definition can often be entirely elided
    -- with Generics or Template Haskell.  Instance   
    -- definitions can generally require minimal or no
    -- implementation either
    class HasAge r age | r -> age where
        getAge :: r -> age
        setAge :: r -> age -> r
    
    isOld :: HasAge r => r -> Bool
    isOld r = getAge r > 30
Firs-class generally means something you can put in a variable and compose with higher order operations. Think first class functions.
> I do agree that there are problems where the "maps of facts" approach is better suited than ADTs. I'm not sure if those are the majority of problems we're solving.

This also seems like the kind of thing where row-types would allow for statically typed, fully inferred programs that are written in the style Rich is advocating.