Search code examples
javascripthaskellfunctional-programmingpattern-matchinggadt

How to implement sum types in dynamically typed languages without pattern matching


I have only a theoretical idea of Haskell's sum types. And yet I sense that they really matter in Haskell and change the way you model your data in a fundamental way. Since I believe that they can be useful in dynamically typed languages as well, I tried to implement a sensible approximation in Javascript (I have only a superficial knowledge of Haskell).

Here is a more or less useful example of a Name sum type, that is able to handle various name formats. I know that Haskell distinguishes between type and data constructors and probably has good reasons to make such distinction. However, I guess that it is not possible to map this concept to Javascript. And pattern matching neither.

Anyway, my actual question is: Does the following implementation embody the nature of sum types and the way they are applied in Haskell?

Please note: I am not sure if this kind of cross-language questions are welcome on SO. Please let me know in case I should avoid them.

// auxiliary functions

const A = f => x => f(x);
const show = api => api.show;

// the type constructor

const Name = (...xs) => A(({length: len}) => {
  switch (len) {
    case 1: {
      let [{length: len}] = xs; // no pattern matching but destructuring

      if (len > 1) { // no Char type in JS
        return k => k({show: () => xs[0]}); // returns the API
      }

      break;
    }

    case 2: {
      let [{length: len}, {length: len2}] = xs;

      if (len > 1 && len2 > 1) {
        return k => k({show: () => `${xs[0]} ${xs[1]}`});
      }

      if (len === 1 && len2 > 1) {
        return k => k({show: () => `${xs[0]}. ${xs[1]}`});
      }

      break;
    }

    case 3: {
      let [{length: len}, {length: len2}, {length: len3}] = xs;

      if (len > 1 && len2 > 1 && len3 > 1) {
        return k => k({show: () => `${xs[0]} ${xs[1]} ${xs[2]}`});
      }

      if (len > 1 && len2 === 1 && len3 > 1) {
        return k => k({show: () => `${xs[0]} ${xs[1]}. ${xs[2]}`});
      }

      if (len === 1 && len2 === 1 && len3 > 1) {
        return k => k({show: () => `${xs[0]}. ${xs[1]}. ${xs[2]}`});
      }
    }

    default: throw new TypeError();
  }
}) (xs);

// run

console.log(Name("Kerouac") (show) ());
console.log(Name("Hans", "Hölzel") (show) ());
console.log(Name("H", "Curry") (show) ());
console.log(Name("Jean", "Luc", "Godard") (show) ());
console.log(Name("William", "S", "Burroughs") (show) ());
console.log(Name("E", "W", "Dijkstra") (show) ());

[EDIT]

Sorry, I should have provided some Haskell code:

type FirstName = String
type LastName = String
type MiddleName = String

data Name = FullName FirstName LastName
  | NameWithMiddle FirstName MiddleName LastName
  | NameWithMiddleInitial FirstName Char LastName
  | TwoInitialsWithLast Char Char LastName
  | OneInitialWithLast Char LastName
  | LastNameOnly LastName

I am not sure if this is valid though.

I guess the problem with my approach is that I try to implement the type constructor Name, while I should implement the value constructors, right?


Solution

  • I think your encoding is a bit too complex because it involves not just the data type itself, but also the show operation.

    The essence of sum types is that they give you a way to create values that represent one or more cases and then write processing code using pattern matching. A slightly simpler version of your example with just full name or a short name would be:

    data Name = 
        FullName String String
      | NickName String
    

    Then you can write a function to process a name by handling the two cases separately:

    case name of
      FullName first last -> "Full name: " ++ first ++ " " ++ last
      NickName nick -> "Nick name: " ++ nick
    

    The key idea is that when you have a name, you can detect which of the two cases it is and write code to handle the two cases separately. In JavaScript, you can do this by including some sort of tag in your value:

    function FullName(first, last) { 
      return { Tag: "FullName", Values: [first, last] };
    }
    function NickName(nick) { 
      return { "Tag": "NickName", Values: [nick] };
    }
    

    Now you can write code akin to pattern matching using switch on the Tag:

    switch(name.Tag) { 
      case "FullName": 
        let [first, last] = name.Values;
        return "Full name: " + first + " " + last;
      case "NickName":
        let [nick] = name.Values
        "Nick name: " + nick;
    }
    

    However, without the language support, you lose many of the nice features:

    • There are no checks when extracting values. If you modify the type and add more fields, then the pattern matching will start failing.
    • There are no checks whether you are covering all cases - when you add another kind of name, the switch statements will not cover that
    • There is no checking on tag names. It is easy to make a typo.

    So, you can certainly use this kind of encoding in JavaScript, but it does not give you as much as in languages that support sum types directly. If there is a more idiomatic JavaScript solution to the problem you are having, it will probably work better.