Java to Haskell in Zero Days

Posted by: Seth Lakowske

Published:

Haskell is a language that is foreign to most programmers coming from an imperitive language like Java, C#, C++, Python and the like. You find new concepts that make learning Haskell feel like learning your first imperitive language. The first step is to acknowledge this, so you don't get frustrated right away when you aren't productive immediately. This acknowledgment allowed me to pass through many frustrating moments trying to do something useful. I realized, I wasn't trying to write a web service, I was trying to learn haskell by way of writing a web service.

If you're a productive Java or C# developer and learning Haskell, you might at times find yourself a little hazy on some of the concepts and terminology. This document is meant to clear up confusion by using terminology you are familiar with to describe Haskell's terminology and syntax.

Learn Haskell by picking a toy application you enjoy

Maybe you like to chain together processes, or write algorithms. Whatever it is, do that so that you have some fun at the end of each struggle. Bite off small toy problems, and when you get too ambitious, scale back and bite off something smaller. When you're stuck for hours, you know it's time to scale back and pick a smaller topic within your toy problem domain to learn.

Java to Haskell translation

Feature Java Haskell
Functions
static <A, B> String f(A a, B b) {
  return "hi";
}
              
f :: a -> b -> String
f a b = "hi"
              
Structs
class MyType {
  String a;
  String b;
}
              
data MyType = MyType {
  a :: String,
  b :: String
}

λ> result = MyType "hi" "world"
λ> a
a :: MyType -> String
λ> a result
"hi"
λ> b result
"world"
              
Typedef
interface PhoneBook extends Iterable<Pair<String,String>>
              
type PhoneBook = [(String, String)]
              
Interface / Type Class
interface Add2 extends Add1 {
  int add2(int number1, int number2);
}

class Calculator implements Add2 {
  public int add2(int n1, int n2) {
    return n1 + n2;
  }
}
              
class (Add1 a) => Add2 a where
  add2 :: a -> Integer -> Integer -> Integer

-- Instance implementation
instance Add2 Calculator where
  add2 calc n1 n2 = n1 + n2
              
Null Handling
String getName() {
  return null; // or actual value
}

String name = getName();
if (name != null) {
  System.out.println(name);
}
              
getName :: Maybe String
getName = Nothing -- or Just "value"

-- Pattern matching
case getName of
  Nothing -> putStrLn "No name"
  Just name -> putStrLn name

-- Or using maybe function
maybe (putStrLn "No name") putStrLn getName
              
Lists/Arrays
List<Integer> numbers =
  Arrays.asList(1, 2, 3, 4, 5);

// Map
List<Integer> doubled = numbers.stream()
  .map(x -> x * 2)
  .collect(Collectors.toList());

// Filter
List<Integer> evens = numbers.stream()
  .filter(x -> x % 2 == 0)
  .collect(Collectors.toList());
              
numbers :: [Integer]
numbers = [1, 2, 3, 4, 5]

-- Map
doubled = map (*2) numbers

-- Filter
evens = filter even numbers

-- List comprehension
evens = [x | x <- numbers, even x]
              
Loops vs Recursion
int sum(List<Integer> numbers) {
  int total = 0;
  for (int n : numbers) {
    total += n;
  }
  return total;
}
              
-- Recursion
sum :: [Integer] -> Integer
sum [] = 0
sum (x:xs) = x + sum xs

-- Or using fold
sum = foldl (+) 0

-- Or using built-in
sum numbers
              
Enums vs ADTs
enum Color {
  RED, GREEN, BLUE
}

enum Result {
  SUCCESS,
  ERROR
}
              
data Color = Red | Green | Blue
  deriving (Show, Eq)

data Result a = Success a | Error String
  deriving (Show, Eq)

-- Usage
myResult :: Result Int
myResult = Success 42
              
Exception Handling
try {
  int result = divide(10, 0);
  System.out.println(result);
} catch (ArithmeticException e) {
  System.out.println("Error: " + e.getMessage());
}
              
divide :: Int -> Int -> Either String Int
divide _ 0 = Left "Division by zero"
divide x y = Right (x `div` y)

-- Using the result
case divide 10 0 of
  Left err -> putStrLn $ "Error: " ++ err
  Right val -> print val
              
Lambda / Anonymous Functions
List<Integer> numbers = Arrays.asList(1,2,3);

numbers.stream()
  .map(x -> x * 2)
  .forEach(x -> System.out.println(x));

// BiFunction
BiFunction<Integer, Integer, Integer> add =
  (a, b) -> a + b;
              
numbers = [1, 2, 3]

-- Lambda syntax
map (\x -> x * 2) numbers

-- Partial application (more idiomatic)
map (*2) numbers

-- Lambda with multiple args
add = \a b -> a + b
-- Or more idiomatically
add a b = a + b
              
Function Composition
// Method chaining
String result = getString()
  .trim()
  .toLowerCase()
  .replace(" ", "_");

// Composed function
Function<String, String> process =
  s -> s.trim().toLowerCase();
              
-- Function composition (.)
-- Reads right to left
process = replace " " "_" . toLower . trim

-- Or using application operator ($)
result = replace " " "_" $ toLower $ trim getString

-- Or using reverse application (&)
result = getString & trim & toLower & replace " " "_"
              
Conditionals
int abs(int x) {
  if (x < 0) {
    return -x;
  } else {
    return x;
  }
}

// Switch
String describe(int x) {
  switch(x) {
    case 0: return "zero";
    case 1: return "one";
    default: return "other";
  }
}
              
-- if-then-else (expression, not statement)
abs x = if x < 0 then -x else x

-- Guards (more idiomatic)
abs x
  | x < 0     = -x
  | otherwise = x

-- Pattern matching
describe :: Int -> String
describe 0 = "zero"
describe 1 = "one"
describe _ = "other"
              
Generics / Type Parameters
class Box<T> {
  private T value;

  public Box(T value) {
    this.value = value;
  }

  public T getValue() {
    return value;
  }
}

Box<String> box = new Box<>("hello");
              
data Box a = Box a
  deriving (Show, Eq)

getValue :: Box a -> a
getValue (Box v) = v

-- Usage
box :: Box String
box = Box "hello"

λ> getValue box
"hello"
              
Collections (Map)
import java.util.HashMap;
import java.util.Map;

Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);

Integer age = ages.get("Alice");
              
import qualified Data.Map as Map

ages :: Map.Map String Integer
ages = Map.fromList [("Alice", 30), ("Bob", 25)]

-- Lookup
age = Map.lookup "Alice" ages  -- Maybe Integer

-- Insert
ages' = Map.insert "Charlie" 35 ages
              
Pattern Matching vs Getters
class Person {
  private String name;
  private int age;

  public String getName() { return name; }
  public int getAge() { return age; }
}

Person p = new Person("Alice", 30);
System.out.println(p.getName());
              
data Person = Person {
  name :: String,
  age :: Int
} deriving Show

-- Automatic accessor functions
p = Person "Alice" 30

λ> name p
"Alice"

-- Pattern matching
greet (Person n a) =
  "Hello " ++ n ++ ", age " ++ show a
              
Static Methods vs Module Functions
class MathUtils {
  public static int square(int x) {
    return x * x;
  }
}

int result = MathUtils.square(5);
              
-- In module MathUtils
module MathUtils (square) where

square :: Int -> Int
square x = x * x

-- Usage
import MathUtils
result = square 5
              
String Operations
String s = "Hello World";
String upper = s.toUpperCase();
String[] words = s.split(" ");
String joined = String.join(",", words);
boolean contains = s.contains("World");
              
import Data.Char (toUpper)
import Data.List (isInfixOf)

s = "Hello World"
upper = map toUpper s
words = split ' ' s  -- using Data.List.Split
joined = intercalate "," words
contains = "World" `isInfixOf` s
              

Haskell documentation

Haskell documentation is found on hackage. After some time you'll be able to read the definitions. Sometimes the author has documented their package well with examples. Until you're more familiar, you'll be better off learning the fundamentals, so go learn you a haskell.