1 of 59

爱因斯坦搞炼丹

Elixir: A Haskeller's Perspective

2 of 59

Bio

知乎@祖与占 (FP/Haskell/PL)

<3 Haskell

Programming Language Fanboy

some experience of Erlang/Elixir

3 of 59

4 of 59

Elixir is a dynamic, functional language designed for building scalable and maintainable applications.

elixir-lang.org

5 of 59

Learn Elixir in Y Minute

  • Elixir -> Erlang Abstract Format -> BEAM Byte code
  • Ruby like, Programmable Syntax (Macro, Special Forms)
  • Immutable
  • Everything is an expression

if 42 do

"foo"

else

"bar"

end

# => "foo"

if 42, do: "foo", else: "bar"

if(42, [{:do, "foo"}, {:else, "bar"}])

6 of 59

Learn Elixir in Y Minute

defmodule Foo.Baz do

@moduledoc “example”

def baz(arg0, arg1) do

Enum.map([{:ok, 1}, {:err, 2}], fn {:err, n} -> {:ok, n - 1} ; a -> a end)

end

end

end

7 of 59

Tricks

Section:

iex > 20 |> Kernel.+(1) |> Kernel.*(2) # Haskell (*2) . (+1) $ (20)

42

Lens:

iex> list = [%{name: "john"}, %{name: "mary"}]

iex> get_and_update_in(list, [Access.all(), :name], fn

...> prev -> {prev, String.upcase(prev)}

...> end)

{["john", "mary"], [%{name: "JOHN"}, %{name: "MARY"}]}

8 of 59

small goal

9 of 59

The Billion Dollar

Mistake

10 of 59

Null References: The Billion Dollar Mistake

11 of 59

Maybe/Either Monad

Haskell (ADT):

data Maybe a = Nothing | Just a

data Either a b = Left a | Right b

Scala (Inheritance):

abstract class Option[+A]

case class Some[+A]() extends Option[A]

case object None extends Option[Nothing]

12 of 59

Poor Elixir :(

  • No ADT
  • No Inheritance

13 of 59

Right way wrong train

Purescript Erlang Backend:

PureScript type

Erlang type

Notes

Tagged union

Tuple with tag element

e.g. Some 42 is {some, 42}

14 of 59

Elixir: Just Tuple!

data Maybe a = Nothing | Just a

-> :error | {:ok, 42}

data Either a b = Left a | Right b

-> {:error, "fail :("} | {:ok, 42}

15 of 59

Callback Case hell, Where is my >>= ?

case foo of

Just bar -> case bar of

Just baz -> case baz of

Just qux -> …

Nothing -> …

Nothing -> …

Nothing -> ...

case foo of

{:ok, bar} -> case bar of

{:ok, bar} -> case baz of

{:ok, qux} -> …

:error -> …

end

:error -> …

End

:error -> ...

end

16 of 59

Railway Oriented Programming

17 of 59

ROP in Elixir using With

else

{:error, reason} -> log_error(reason)

_ -> handle_ambigous_error

end

with {:ok, output0} <- do_sth0(input0),

{:ok, output1} <- do_sth1(input1)

output1

18 of 59

Derailment

defmodule File do

@spec read(Path.t) :: {:ok, binary} | {:error, posix}

def read(path) do

...

end

@spec read!(Path.t) :: binary | no_return

def read!(path) do # Scheme :D

end

end

19 of 59

Haskell Pitfalls No.1: Partial function

[]

[a]

a

head

[]

[a]

Just a

headMay

Nothing

Partial

Total

20 of 59

Partial function: a legacy from Erlang

iex(1)> hd []

** (ArgumentError) argument error

:erlang.hd([])

@spec hd(nonempty_maybe_improper_list(elem, any)) :: elem when elem: term

def hd(list) do

:erlang.hd(list)

end

21 of 59

A Little Syntax

22 of 59

Sigil

23 of 59

Sigil

In computer programming, a sigil (/ˈsɪdʒəl/) is a symbol attached to a variable name, showing the variable's datatype or scope, usually a prefix, as in $foo, where $ is the sigil. (wikipedia)

Scope:

Ruby(Perl): $GLOBAL_VAR, @instance_var, @@class_var

Data type:

Python: r"[regex]", u"unicode"

C#: @"\\server\share\file.txt"

24 of 59

Elixir: Sigil (Ruby)

defmacro sigil_w(term, modifiers) do

…...

end�

iex> ~w(--source test/enum_test.exs)

["--source", "test/enum_test.exs"]

iex> ~w(foo bar baz)a

[:foo, :bar, :baz]

iex> ~T[13:00:07] # times

~T[13:00:07]

iex> Regex.match?(~r(foo), "foo")

true

iex> Regex.match?(~r/abc/, "abc")

true

25 of 59

Haskell: OverloadedStrings?

{-# LANGUAGE OverloadedStrings #-}

a :: String

a = "hello"

b :: Text

b = "hello"

26 of 59

Template Haskell! Q(uasi)Q(uoter)

regexqq: [$rx|([aeiou]).*(er|ing|tion)([\.,\?]*)$|]

raw-strings-qq: [r|C:\Windows\SYSTEM|] ++ [r|\user32.dll|]

ruby-qq: [x|echo >&2 "Hello, world!"|]

aeason-qq: [aesonQQ| {age: 23, name: "John", likes: ["linux", "Haskell"]} |]

27 of 59

Bonus: Record puns in Elixir

{-# LANGUAGE NamedFieldPuns #-}

greet IndividualR { person = PersonR { firstName = fn } } = "Hi, " ++ fn

greet IndividualR { person = Person { firstName } } = "Hi, " ++ firstName

foo = 1

bar = 2

~m(foo bar)a == %{foo: foo, bar: bar}

28 of 59

Protocol

29 of 59

Enumerable, Collectable

Reducees

30 of 59

Protocol & Behaviour

Protocol(...like typeclass?)

  • For data type
  • Dispatching!

defprotocol Size do

def size(data)

end

defimpl Size, for: Tuple do

def size(tuple), do: tuple_size(tuple)

end

Behaviour(...like interface?):

  • For Module
  • Compile time

defmodule Parser do

@callback parse(String.t) :: any

@callback extensions() :: [String.t]

end

defmodule JSONParser do

@behaviour Parser

def parse(str), do: # ... parse JSON

def extensions, do: ["json"]

end

31 of 59

Enumerable

Elixir provides the concept of collections, which may be in-memory data structures, as well as events, I/O resources and more. Those collections are supported by the Enumerable protocol, which is an implementation of an abstraction we call “reducees”.

-- Introducing reducees

32 of 59

defprotocol Enumerable do

@type acc :: {:cont, term} | {:halt, term} | {:suspend, term}

@type reducer :: (term, term -> acc)

@type result :: {:done, term} | {:halted, term} | {:suspended, term, continuation}

@type continuation :: (acc -> result)

@spec reduce(t, acc, reducer) :: result

def reduce(enumerable, acc, fun)

end

33 of 59

Iterator (ask)

def next([x|xs]) do

{x, xs}

end

def next([]) do

:done

end

def map(collection, fun) do

map_next(next(collection), fun)

end

defp map_next({x, xs}, fun) do

[fun.(x)|map_next(next(xs), fun)]

end

defp map_next(:done, _fun) do

[]

end

34 of 59

Iterator: Resource Management Problem(tell)

map parse [Line0, Line1, Line2, …] #fs

Parse Error!

fs = File.stream(path)

35 of 59

Iterator: halt & try...catch...

defp map_next({h, t}, fun) do

[try do

fun.(h)

rescue

e ->

halt(t)

raise(e)

end|map_next(next(t), fun)]

end

def take(collection, n) do

take_next(next(collection), n)

end

...

defp take_next(:done, _n) do: []

defp take_next(value, 0) do

halt(value) # side-effect

end

36 of 59

Reducer (Clojure)

defmodule Reducer do

def reduce([x|xs], acc, fun) do

reduce(xs, fun.(x, acc), fun)

end

def reduce([], acc, _fun) do

acc

end

end

"the only thing that knows how to apply a function to a collection is the collection itself"

37 of 59

Reducer: Good & Bad

def reduce(file, acc, fun) do

descriptor = File.open(file)

try do

reduce_next(IO.readline(descriptor), acc, fun)

after

File.close(descriptor)

end

end

...

def take(collection, n) do

# purely functional way?

end

38 of 59

Iteratee (Haskell)

defmodule Iteratee do

def enumerate([h|t], {:cont, fun}) do

enumerate(t, fun.({:some, h}))

end

def enumerate([], {:cont, fun}) do

fun.(:done)

end

def enumerate(_, {:halt, acc}) do

{:halted, acc}

end

end

39 of 59

Iteratee & map

def map(collection, fun) do

{:done, acc} = enumerate(collection, {:cont, mapper([], fun)})

:lists.reverse(acc)

end

defp mapper(acc, fun) do

fn

{:some, h} -> {:cont, mapper([fun.(h)|acc], fun)}

:done -> {:done, acc}

end

end

40 of 59

Iteratee & map

def map(collection, fun) do

{:done, acc} = enumerate(collection, {:cont, mapper([], fun)})

:lists.reverse(acc)

end

defp mapper(acc, fun) do

fn

{:some, h} -> {:cont, mapper([fun.(h)|acc], fun)}

:done -> {:done, acc}

end

end

41 of 59

Reducees

defmodule Reducee do

def reduce([h|t], {:cont, acc}, fun) do

reduce(t, fun.(h, acc), fun)

end

def reduce([], {:cont, acc}, _fun) do

{:done, acc}

end

def reduce(_, {:halt, acc}, _fun) do

{:halted, acc}

end

end

42 of 59

Reducees & map

def map(collection, fun) do

{:done, acc} =

reduce(collection, {:cont, []}, fn x, acc ->

{:cont, [fun.(x)|acc]}

end)

:lists.reverse(acc)

end

43 of 59

Reducees & take

def take(collection, n) when n > 0 do

{_, {acc, _}} =

reduce(collection, {:cont, {[], n}}, fn

x, {acc, count} -> {take_instruction(count), {[x|acc], n-1}}

end)

:lists.reverse(acc)

end

defp take_instruction(1), do: :halt

defp take_instruction(n), do: :cont

44 of 59

Collectable

iex> Enum.map(%{x: 1, y: 2}, fn {a,b} -> {a, b+1} end)

[x: 2, y: 3] # : (

iex> Enum.map(%{x: 1, y: 2}, fn {a,b} -> {a, b+1} end) |> Enum.into(%{})

%{x: 2, y: 3}

Haskell:

Prelude Data.Map> fromList [("x", 1), ("y", 2)]

fromList [("x",1),("y",2)]

45 of 59

Binary

46 of 59

Binary Pattern Matching

View Pattern

47 of 59

Char List(Haskell String) and Binary (Bytestring)

iex> i 'abc'

Term

'abc'

Data type

List

iex> 'hełło'

[104, 101, 322, 322, 111]

iex> i "abc"

Term

"abc"

Data type

BitString

iex> string = "hełło"

"hełło"

iex> byte_size(string)

7

iex> String.length(string)

5

48 of 59

Bitstring, Binaries & Strings

Bitstring: sequence of bits

V

Binary: sequence of bytes

V

String: UTF-8 encoded binary

49 of 59

<<args>> - Defines a new bitstring

iex> <<name::binary-size(5), " the ", species::binary>> = <<"Frank the Walrus">>

"Frank the Walrus"

iex> {name, species}

{"Frank", "Walrus"}

50 of 59

Binary Pattern Matching in Haskell?

Bytestring --------------------> Intermedia -----------------------------> Result

parse function + Pattern Matching!

parse

pattern matching

51 of 59

View Pattern

View patterns extend our ability to pattern match on variables by also allowing us to pattern match on the result of function application.

-- 24 Days of GHC Extensions: View Patterns

52 of 59

View Pattern (GHC Users Guide)

type Typ

data TypView = Unit

| Arrow Typ Typ

view :: Typ -> TypView

size :: Typ -> Integer

size t = case view t of

Unit -> 1

Arrow t1 t2 -> size t1 + size t2

{-# LANGUAGE ViewPatterns #-}

size (view -> Unit) = 1

size (view -> Arrow t1 t2) = size t1 + size t2

53 of 59

Binary Pattern Matching in Haskell

Elixir:

<<name::binary-size(5), " the ", species::binary>> = <<"Frank the Walrus">>

Haskell:

splitAt :: Int -> ByteString -> (ByteString, ByteString)

stripSuffix :: ByteString -> ByteString -> Maybe ByteString

parse (sprintAt 5 -> (name, stripPrefix(" the ") -> Just species)) = (name, species)

54 of 59

Pretty Print

55 of 59

Inspect

Inspect Algebra

56 of 59

Inspect Protocol

iex(1)> inspect [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], pretty: true, width: 10

"[1, 2,\n 3, 4,\n 5, 6,\n 7, 8,\n 9,\n 10]"

57 of 59

Inspect.Algebra

A set of functions for creating and manipulating algebra documents.This module implements the functionality described in “Strictly Pretty” (2000) by Christian Lindig with small additions, like support for String nodes, and a custom rendering function that maximises horizontal space use.

58 of 59

History

  • Lazy v. Yield: Incremental, Linear Pretty-printing (Oleg Kiselyov, Simon Peyton-Jones, and Amr Sabry, 2012)
  • PPrint, a prettier printer(Daan Leijen, 2001)
  • Strictly Pretty (Christian Lindig, 2000)
  • A Prettier Printer (Philip Wadler, 1997)
  • A pretty printer library in Haskell (Simon Peyton Jones, 1997)
  • The Design of Pretty-Printing Library (John Hughes, 1995)

wl-pprint

pretty

TIME’s UP! :D

59 of 59

Reference