爱因斯坦搞炼丹
Elixir: A Haskeller's Perspective
Bio
知乎@祖与占 (FP/Haskell/PL)
<3 Haskell
Programming Language Fanboy
some experience of Erlang/Elixir
Elixir is a dynamic, functional language designed for building scalable and maintainable applications.
elixir-lang.org
Learn Elixir in Y Minute
if 42 do
"foo"
else
"bar"
end
# => "foo"
if 42, do: "foo", else: "bar"
if(42, [{:do, "foo"}, {:else, "bar"}])
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
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"}]}
small goal
The Billion Dollar
Mistake
Null References: The Billion Dollar Mistake
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]
Poor Elixir :(
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} |
Elixir: Just Tuple!
data Maybe a = Nothing | Just a
-> :error | {:ok, 42}
data Either a b = Left a | Right b
-> {:error, "fail :("} | {:ok, 42}
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
Railway Oriented Programming
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
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
Haskell Pitfalls No.1: Partial function
[]
[a]
a
head
[]
[a]
Just a
headMay
Nothing
Partial
Total
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
A Little Syntax
Sigil
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"
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
Haskell: OverloadedStrings?
{-# LANGUAGE OverloadedStrings #-}
a :: String
a = "hello"
b :: Text
b = "hello"
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"]} |]
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}
Protocol
Enumerable, Collectable
Reducees
Protocol & Behaviour
Protocol(...like typeclass?)
defprotocol Size do
def size(data)
end
defimpl Size, for: Tuple do
def size(tuple), do: tuple_size(tuple)
end
Behaviour(...like interface?):
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
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
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
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
Iterator: Resource Management Problem(tell)
map parse [Line0, Line1, Line2, …] #fs
Parse Error!
fs = File.stream(path)
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
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"
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
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
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
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
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
Reducees & map
def map(collection, fun) do
{:done, acc} =
reduce(collection, {:cont, []}, fn x, acc ->
{:cont, [fun.(x)|acc]}
end)
:lists.reverse(acc)
end
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
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)]
Binary
Binary Pattern Matching
View Pattern
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
Bitstring, Binaries & Strings
Bitstring: sequence of bits
V
Binary: sequence of bytes
V
String: UTF-8 encoded binary
<<args>> - Defines a new bitstring
iex> <<name::binary-size(5), " the ", species::binary>> = <<"Frank the Walrus">>
"Frank the Walrus"
iex> {name, species}
{"Frank", "Walrus"}
Binary Pattern Matching in Haskell?
Bytestring --------------------> Intermedia -----------------------------> Result
parse function + Pattern Matching!
parse
pattern matching
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
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
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)
Pretty Print
Inspect
Inspect Algebra
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]"
�
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.
History
wl-pprint
pretty
TIME’s UP! :D