Quantcast
Channel: Blog – Sumo Logic
Viewing all articles
Browse latest Browse all 1036

Dirty Haskell Phrasebook

$
0
0

Whenever people ask me whether Hungarian is difficult to learn, I half-jokingly say that it can’t be too hard given that I had learned it by the time I turned three. Having said that, I must admit that learning a new language as a grown-up is a whole new ball game. Our struggle for efficiency is reflected in the way we learn languages: we focus on the most common patterns, and reuse what we know as often as possible.

Programming languages are no different. When I started at Sumo Logic just two months ago, I wanted to become fluent in Scala as quickly as possible. Having a soft spot for functional languages such as Haskell, a main factor in deciding to do an internship here was that we use Scala. I soon realized that a large subset of Haskell can easily be translated into Scala, which made the learning process a lot smoother so far.

You’ve probably guessed by now that this post is going to be a Scala phrasebook for Haskellers. I’m also hoping that it will give new insights to seasoned Scalaists, and spark the interest of programmers who are new to the functional paradigm. Here we go.

Basics

module Hello where
 
main :: IO ()
main = do
  putStrLn "Hello, World!"
object Hello {
 
  def main(args: Array[String]): Unit =
    println("Hello, World!")
}
 

While I believe that HelloWorld examples aren’t really useful, there are a few key points to make here.

The object keyword creates a singleton object with the given name and properties. Pretty much everything in Scala is an object, and has its place in the elaborate type hierarchy stemming from the root-type called Any. In other words, a set of types always has a common ancestor, which isn’t the case in Haskell. One consequence of this is that Scala’s ways of emulating heterogeneous collections are more coherent. For example, Haskell needs fairly involved machinery such as existential types to describe a list-type that can simultaneously hold elements of all types, which is simply Scala’s List[Any].

In Scala, every function (and value) needs an enclosing object or class. (In other words, every function is a method of some object.) Since object-orientation concepts don’t have direct analogues in Haskell, further examples will implicitly assume an enclosing object on the Scala side.

Haskell’s () type is Scala’s Unit, and its only value is called () just like in Haskell. Scala has no notion of purity, so functions might have side-effects without any warning signs. One particular case is easy to spot though: the sole purpose of a function with return type Unit is to exert side effects.

Values

answer :: Int
answer = 42
lazy val answer: Int = 42
 

Evaluation in Haskell is non-strict by default, whereas Scala is strict. To get the equivalent of Haskell’s behavior in Scala, we need to use lazy values (see also lazy collections). In most cases however, this makes no difference. From now on, the lazy keyword will be dropped for clarity. Besides val, Scala also has var which is mutable, akin to IORef and STRef in Haskell.

Okay, let’s see values of some other types.

question :: [Char]
question = "What's six by nine?"
val question: String =
  "What's six by nine?"
 

Can you guess what the type of the following value is?

judgement = (6*9 /= 42)
val judgement = (6*9 != 42)
 

Well, so can Haskell and Scala. Type inference makes it possible to omit type annotations. There are a few corner cases that get this mechanism confused, but a few well-placed type annotations will usually sort those out.

Data Structures

Lists and tuples are arguably the most ubiquitous data structures in Haskell.

In contrast with Haskell’s syntactic sugar for list literals, Scala’s notation seems fairly trivial, but in fact involves quite a bit of magic under the hood.

list :: [Int]
list = [3, 5, 7]
val list: List[Int] = List(3, 5, 7)
 

Lists can also be constructed from a head-element and a tail-list.

smallPrimes = 2 : list
val smallPrimes = 2 :: list
 

As you can see, : and :: basically switched roles in the two languages. This list-builder operator, usually called cons, will come in handy when we want to pattern match on lists (see Control Structures and Scoping below for pattern matching).

Common accessors and operations have the same name, but they are methods of the List class in Scala.

head list
list.head
tail list
list.tail
map func list
list.map(func)
zip list_1 list_2
list_1.zip(list_2)
 

If you need to rely on the non-strict evaluation semantics of Haskell lists, use Stream in Scala.

Tuples are virtually identical in the two languages.

tuple :: ([Char], Int)
tuple = (question, answer)
val tuple: (String, Int) =
  (question, answer)
 

Again, there are minor differences in Scala’s accessor syntax due to object-orientation.

fst tuple
tuple._1
snd tuple
tuple._2
 

Another widely-used parametric data type is Maybe, which can represent values that might be absent. Its equivalent is Option in Scala.

singer :: Maybe [Char]
singer = Just "Carly Rae Jepsen"
val singer: Option[String] =
  Some("Carly Rae Jepsen")
song :: Maybe [Char]
song = Nothing
val song: Option[String] =
  None
 

Algebraic data types translate to case classes.

data Tree
  = Leaf
  | Branch [Tree]
  deriving (Eq, Show)
sealed abstract class Tree
case class Leaf extends Tree
case class Branch(kids: List[Tree]) extends Tree
 

Just like their counterparts, case classes can be used in pattern matching (see Control Structures and Scoping below), and there’s no need for the new keyword at instantiation. We also get structural equality check and conversion to string for free, in the form of the equals and toString methods, respectively.

The sealed keyword prevents anything outside this source file from subclassing Tree, just to make sure exhaustive pattern lists don’t become undone.

See also extractor objects for a generalization of case classes.

Functions

increment :: Int -> Int
increment x = x + 1
def increment(x: Int): Int = x + 1
 

If you’re coming from a Haskell background, you’re probably not surprised that the function body is a single expression. For a way to create more complex functions, see let-expressions in Control Structures and Scoping below.

three = increment 2
val three = increment(2)
 

Most of the expressive power of functional languages stems from the fact that functions are values themselves, which leads to increased flexibility in reusing algorithms.

Composition is probably the simplest form of combining functions.

incrementTwice =
  increment . increment
val incrementTwice =
  (increment: Int => Int).compose(increment)
 

Currying, Partial Application, and Function Literals

Leveraging the idea that functions are values, Haskell chooses to have only unary functions and emulate higher arities by returning functions, in a technique called currying. If you think that isn’t a serious name, you’re welcome to call it schönfinkeling instead.

Here’s how to write curried functions.

addCurry :: Int -> Int -> Int
addCurry x y = x + y
def addCurry(x: Int)(y: Int): Int =
  x + y
 
five = addCurry 2 3
val five = addCurry(2)(3)
 

The rationale behind currying is that it makes certain cases of partial application very succinct.

addSix :: Int -> Int
addSix = addCurry 6
val addSix: Int => Int =
  addCurry(6)
 
val addSix = addCurry(6) : (Int => Int)
 
val addSix = addCurry(6)(_)
 

The type annotation is needed to let Scala know that you didn’t forget an argument but really meant partial application. If you want to drop the type annotation, use the underscore placeholder syntax.

To contrast with curried ones, functions that take many arguments at once are said to be uncurried. Scalaists seem to prefer their functions less spicy by default, most likely to save parentheses.

addUncurry :: (Int, Int) -> Int
addUncurry (x, y) = x + y
def addUncurry(x: Int, y: Int): Int =
  x + y
 
seven = addUncurry (2, 5)
val seven = addUncurry(2, 5)
 

Uncurried functions can still be partially applied with ease in Scala, thanks to underscore placeholder notation.

addALot :: Int -> Int
addALot =
  \x -> addUncurry (x, 42)
val addALot: Int => Int =
  addUncurry(_, 42)
 
val addALot =
  addUncurry(_: Int, 42)
 

When functions are values, it makes sense to have function literals, a.k.a. anonymous functions.

(brackets :: Int -> [Char]) =
  \x -> "<" ++ show x ++ ">"
val brackets: Int => String =
  x => "<%s>".format(x)
brackets = \(x :: Int) ->
  "<" ++ show x ++ ">"
val brackets =
  (x: Int) => "<%s>".format(x)
 

Infix Notation

In Haskell, any function whose name contains only certain operator characters will take its first argument from the left side when applied, which is infix notation if it has two arguments. Alphanumeric function names surrounded by backticks also behave that way. In Scala, any single-argument function can be used as an infix operator by omitting the dot and parentheses from the function call syntax.

data C = C [Char]
 
bowtie (C s) t =
  s ++ " " ++ t
 
(|><|) = bowtie
case class C(s: String) {
 
  def bowtie(t: String): String =
    s + " " + t
 
  val |><| = bowtie(_)
}
(C "James") |><| "Bond"
C("James") |><| "Bond"
(C "James") `bowtie` "Bond"
C("James") bowtie "Bond"
 

Haskell’s sections provide a way to create function literals from partially applied infix operators. They can then be translated to Scala using placeholder notation.

tenTimes = (10*)
val tenTimes = 10 * (_: Int)
 

Again, the type annotation is necessary so that Scala knows you meant what you wrote.

Higher-order Functions and Comprehensions

Higher order functions are functions that have arguments which are functions themselves. Along with function literals, they can be used to express complex ideas in a very compact manner. One example is operations on lists (and other collections in Scala).

map (3*) (filter (<5) list)
list.filter(_ < 5).map(3 * _)
 

That particular combination of map and filter can also be written as a list comprehension.

[3 * x | x <- list, x < 5]
for(x <- list if x < 5) yield (3 * x)
 

Control Structures and Scoping

Pattern matching is a form of control transfer in functional languages.

countNodes :: Tree -> Int
countNodes t = case t of
  Leaf -> 1
  (Branch kids) ->
    1 + sum (map countNodes kids)
def countNodes(t: Tree): Int =
  t match {
    case Leaf() => 1
    case Branch(kids) =>
      1 + kids.map(countNodes).sum
  }
 

For a definition of Tree, see the Data Structures section above.

Even though they could be written as pattern matching, if-expressions are also supported for increased readability.

if condition
  then expr_0
  else expr_1
if (condition)
  expr_0
else
  expr_1
 

Let expressions are indispensable in organizing complex expressions.

result =
  let
    v_0 = bind_0
    v_1 = bind_1
    -- ...
    v_n = bind_n
  in
   expr
val result = {
 
  val v_0 = bind_0
  val v_1 = bind_1
  // ...
  val v_n = bind_n
 
  expr
}
 

A code block evaluates to its final expression if the control flow reaches that point. Curly brackets are mandatory; Scala isn’t indentation-sensitive.

Parametric Polymorphism

I’ve been using parametric types all over the place, so it’s time I said a few words about them. It’s safe to think of them as type-level functions that take types as arguments and return types. They are evaluated at compile time.

[a]
List[A]
(a, b)
(A, B)
 
// desugars to
Tuple2[A, B]
Maybe a
Option[A]
a -> b
A => B
 
// desugars to
Function1[A, B]
a -> b -> c
A => B => C
 
// desugars to
Function2[A, B, C]
 

Type variables in Haskell are required to be lowercase, whereas they’re usually uppercase in Scala, but this is only a convention.

In this context, Haskell’s type classes loosely correspond to Scala’s traits, but that’s a topic for another time. Stay tuned.

Comments

-- single-line comment
// single-line comment
{-
Feel free to suggest additions
and corrections to the phrasebook
in the comments section below. :]
-}
/*
Feel free to suggest additions
and corrections to the phrasebook
in the comments section below. :]
*/
 

Here Be Dragons

Please keep in mind that this phrasebook is no substitute for the real thing; you will be able to write Scala code, but you won’t be able to read everything. Relying on it too much will inevitably yield some unexpected results. Don’t be afraid of being wrong and standing corrected, though. As far as we know, the only path to a truly deep understanding is the way children learn: by poking around, breaking things, and having fun.

 

Viewing all articles
Browse latest Browse all 1036

Trending Articles