Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing Algebraic Data Type in Scala

In Haskell, I can define a Tree:

data Tree a = Empty | Node a (Tree a) (Tree a) 

How could I write this in Scala?

I'm not sure how to keep the type parameter [A] in Scala for Node to match Tree's type, a.

like image 892
Kevin Meredith Avatar asked Nov 01 '14 13:11

Kevin Meredith


People also ask

What is algebraic data type in Scala?

What Are Algebraic Data Types? ADTs are commonly used in Scala. Simply put, an algebraic data type is any data that uses the Product or Sum pattern. The question now shifts to understanding what Product and Sum Types are.

Does Scala have sum types?

Sum Type. The sum type in Scala is implemented by way of inheritance. The values of the sum type can are subtypes associated with logical disjunctions (ORs), where the possible values are this subtype OR that subtype … Here we create a sum type from Option that can either be None , or a Some containing a tuple.

What is algebraic data type in Java?

In programming languages, it carries a similar meaning. An algebraic data type is a composite containing variables. a composite can further contain other types as variables as well. A recursive type can contain another instance of itself as a variable.

Does Python have algebraic data types?

adt. adt is a library providing algebraic data types in Python, with a clean, intuitive syntax, and support for typing through a mypy plugin.


2 Answers

Defining an ADT

In Scala's "object-functional" model, you define a trait that represents the ADT and all of its parameters. Then for each of your cases, you define either a case class or a case object. Type and value parameters are treated as arguments to the class constructor. Typically, you make the trait sealed so that nothing outside the current file can add cases.

sealed trait Tree[A] case class Empty[A]() extends Tree[A] case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A] 

Then you can do:

scala> Node("foo", Node("bar", Empty(), Empty()), Empty()) res2: Node[String] = Node(foo,Node(bar,Empty(),Empty()),Empty()) 

It's kind of annoying that we have to create a whole bunch of new Empty instances, when that class carries no data. In Scala, it's common practice to replace a zero-argument case class, like Empty, with a case object, although in this case, it's a little tricky, because a case object is a singleton, but we need an Empty for every type of tree.

Fortunately (or not, depending on who you ask), with a covariance annotation, you can have one case object Empty act as the empty Tree of type Nothing, which is Scala's universal subtype. Due to covariance, this Empty is now a subtype of Tree[A] for all possible A:

sealed trait Tree[+A] case object Empty extends Tree[Nothing] case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A] 

Then you get the cleaner syntax:

scala> Node("foo", Node("bar", Empty, Empty), Empty) res4: Node[String] = Node(foo,Node(bar,Empty,Empty),Empty) 

This is, in fact, how Scala's standard library Nil works, with respect to List.

Operating on an ADT

To use the new ADT, it's common in Scala to define recursive functions that employ the match keyword to deconstruct it. See:

scala> :paste // Entering paste mode (ctrl-D to finish)  import scala.math.max def depth[A](tree: Tree[A]): Int = tree match {   case Empty => 0   case Node(_, left, right) => 1 + max(depth(left), depth(right)) }  // Exiting paste mode, now interpreting.  import scala.math.max depth: [A](tree: Tree[A])Int  scala> depth(Node("foo", Node("bar", Empty, Empty), Empty)) res5: Int = 2 

Scala characteristically gives the developer a bewildering array of options to choose from in how to organize functionality that operates on ADTs. I can think of four basic approaches.

1) You can make it a standalone function external to the trait:

sealed trait Tree[+A] case object Empty extends Tree[Nothing] case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A]  object Tree {   def depth[A](tree: Tree[A]): Int = tree match {     case Empty => 0     case Node(_, left, right) => 1 + max(depth(left), depth(right))   } } 

This might be nice if you want your API to feel more functional than object-oriented, or if your operation might product an instance of your ADT from other data. The companion object is often a natural place to put such methods.

2) You can make it a concrete method of the trait itself:

sealed trait Tree[+A] {   def depth: Int = this match {     case Empty => 0     case Node(_, left, right) => 1 + max(left.depth, right.depth)   } } case object Empty extends Tree[Nothing] case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A] 

This is particularly useful if your operation can be defined purely in terms of other methods of the trait, in which case you probably won't explicitly use match.

3) You can make it an abstract method of the trait with concrete implementations in the subtypes (obviating the need to use match):

sealed trait Tree[+A] {   def depth: Int } case object Empty extends Tree[Nothing] {   val depth = 0 }  case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A] {   def depth = 1 + max(left.depth, right.depth) } 

This is most similar to the approach traditional object-oriented polymorphism. It feels natural to me when defining the low-level operations of the trait, with richer functionality defined in terms of these operations in the trait itself. It's also most appropriate when working with traits that aren't sealed.

4) Or, in the case you want to add a method to an ADT whose source is external to your project, you could use an implicit conversion to a new type that has the method:

// assuming Tree defined elsewhere implicit class TreeWithDepth[A](tree: Tree[A]) {   def depth: Int = tree match {     case Empty => 0     case Node(_, left, right) => 1 + max(left.depth, right.depth)   } } 

This is a particularly handy way to enhance types defined in code you don't control, to factor auxiliary behavior out of your types so that they can be focused on core behavior, or to facilitate of ad hoc polymorphism.

Method 1 is a function that takes a Tree and works like the first example. Methods 2-4 are all operations on a Tree:

scala> Node("foo", Node("bar", Empty, Empty), Empty).depth res8: Int = 2 
like image 92
acjay Avatar answered Sep 30 '22 15:09

acjay


Starting in Scala 3, and the new union type, this will become possible:

type Tree[A] = Node[A] | Empty.type case object Empty case class Node[A](value: A, left: Tree[A], right: Tree[A]) 

which you can instantiate as such:

val empty: Tree[String] = Empty val tree:  Tree[String] = Node("foo", Node("bar", Empty, Empty), Empty) 

and use as part of a concrete example:

def depth[A](tree: Tree[A]): Int =   tree match {     case Empty                => 0     case Node(_, left, right) => 1 + (depth(left) max depth(right))   }  depth(tree)  // 2 depth(empty) // 0 
like image 25
Xavier Guihot Avatar answered Sep 30 '22 15:09

Xavier Guihot