Winn is a dynamically typed, functional language that compiles to the BEAM (Erlang VM). Syntax is inspired by Ruby and Elixir.
Every Winn file contains one or more modules. A module is the top-level unit of code organization.
module Greeter
def greet(name)
IO.puts("Hello, " <> name <> "!")
end
end
Module names are capitalized. They compile to lowercase Erlang module atoms (Greeter → :greeter).
Modules can use dotted names for hierarchical organization:
module MyApp.Router
def routes()
[{:get, "/", :index}]
end
end
Dotted names compile to dotted atoms (MyApp.Router → :'myapp.router').
Module names can be passed as values to functions. They compile to lowercase atoms matching the compiled module name:
Repo.insert(Post, changeset) # Post becomes the atom :post
Repo.all(Contact) # Contact becomes :contact
import brings a module's functions into scope as local calls:
module MyApp
import Enum
def run()
map([1,2,3]) do |x| x * 2 end # instead of Enum.map(...)
end
end
Local functions take priority — if you define a function with the same name as an imported one, your local version is called.
alias lets you use a short name for a dotted module path:
module MyApp
alias MyApp.Auth
def run()
Auth.verify("token") # instead of MyApp.Auth.verify(...)
end
end
The short name is the last segment: alias MyApp.Auth makes Auth available.
Functions are defined with def and closed with end. The last expression in a function body is the return value.
Function names can end with ? for predicates:
def valid?(changeset)
Changeset.valid(changeset)
end
# Standard library predicates:
List.contains?(2, [1, 2, 3]) # => true
Map.has_key?(user, :name) # => true
Enum.any?([1, 2, 3]) do |x| x > 2 end # => true
module Math
def add(a, b)
a + b
end
def square(n)
n * n
end
end
Parameters can have default values. When called with fewer arguments, defaults are filled in:
def greet(name, greeting = "Hello")
"#{greeting}, #{name}!"
end
greet("Alice") # => "Hello, Alice!"
greet("Alice", "Hi") # => "Hi, Alice!"
Multiple defaults are supported — they must come after required parameters:
def connect(host, port = 5432, timeout = 5000)
# ...
end
connect("localhost") # port=5432, timeout=5000
connect("localhost", 3306) # timeout=5000
connect("localhost", 3306, 10000)
Defaults can be strings, integers, floats, atoms, and booleans.
Define multiple clauses for pattern-based dispatch:
module Greeter
def greet(:world)
"Hello, World!"
end
def greet(name)
"Hello, " <> name <> "!"
end
end
Clauses are matched top-to-bottom.
42
3.14
-100
Strings are UTF-8 binaries. Concatenate with <> or use interpolation:
"Hello, " <> "World!"
"Hello, #{name}!"
Embed any expression inside #{} within a double-quoted string:
name = "Alice"
IO.puts("Hello, #{name}!")
count = 42
IO.puts("There are #{to_string(count)} items")
IO.puts("#{to_string(1 + 2)} is three")
Escape # with a backslash to prevent interpolation: "\#{not interpolated}"
Atoms are prefixed with ::
:ok
:error
:hello
true
false
nil
[1, 2, 3]
["alice", "bob", "carol"]
[]
{:ok, value}
{:error, "not found"}
{:user, "Alice", 30}
%{name: "Alice", age: 30}
%{status: :active}
a + b
a - b
a * b
a / b
"Hello, " <> name
a == b
a != b
a < b
a > b
a <= b
a >= b
a and b
a or b
not a
Variables are bound with =. They are immutable bindings (like Elixir):
x = 42
name = "Alice"
result = x + 10
Destructure tuples on the left side of =:
{:ok, value} = {:ok, 42}
# value is now 42
{:ok, {a, b}} = {:ok, {1, 2}}
# a is 1, b is 2
If the pattern doesn't match, it raises a runtime error.
The |> operator passes the result of the left expression as the first argument to the right:
"hello world"
|> String.upcase()
|> IO.puts()
Is equivalent to:
IO.puts(String.upcase("hello world"))
Pipes chain naturally:
def process(list)
list
|> Enum.filter() do |x| x > 0 end
|> Enum.map() do |x| x * 2 end
end
Match on tuples, atoms, integers, and lists in function parameters:
module Result
def unwrap({:ok, value})
value
end
def unwrap({:error, reason})
IO.puts("Error: " <> reason)
:error
end
end
module Shape
def area({:circle, r})
3.14159 * r * r
end
def area({:rect, w, h})
w * h
end
end
Use _ to ignore a value:
def handle_info(_, state)
{:noreply, state}
end
match...end desugars to a case expression. Use after a pipe or with an explicit scrutinee:
%% Pipe into match
result
|> match
ok value => value
err msg => IO.puts("Error: " <> msg)
end
%% Standalone match with scrutinee
match response
ok data => IO.puts("Got: " <> data)
err code => IO.puts("Failed")
end
ok val matches {:ok, val}. err e matches {:error, e}.
Pass anonymous functions to iterators using do |params| ... end syntax:
Enum.map(list) do |x|
x * 2
end
Enum.filter(list) do |x|
x > 0
end
Enum.reduce(list, 0) do |x, acc|
x + acc
end
Combine with pipes:
list
|> Enum.filter() do |x| x > 1 end
|> Enum.map() do |x| x * 10 end
Capture the result of a pipe chain into a variable:
[1, 2, 3, 4, 5]
|> Enum.filter() do |x| x > 2 end
|> Enum.map() do |x| x * 10 end
|>= results
IO.puts("Got #{to_string(List.length(results))} results")
|>= assigns the pipe result to the named variable. The variable is available in subsequent expressions.
Use """...""" for multi-line strings. Common leading whitespace is stripped automatically, and embedded " quotes don't need escaping:
sql = """
SELECT *
FROM users
WHERE active = true
ORDER BY created_at DESC
"""
html = """
<div class="card">
<h1>#{title}</h1>
</div>
"""
Triple-quoted strings support interpolation (#{}) just like regular strings.
Define named struct types with struct:
module User
struct [:name, :email, :age]
end
This generates:
User.new()— returns a map with all fields set toniland a__struct__keyUser.new(%{name: "Alice", age: 30})— merges attributes into the default mapUser.__struct__()— returns the module atom (for type identification)User.__fields__()— returns the list of field names
user = User.new(%{name: "Alice", age: 30})
user.name # => "Alice"
user.__struct__ # => :user
Structs are maps with a __struct__ key, so all Map functions work on them. You can define methods alongside the struct:
module User
struct [:name, :email]
def greet(user)
"Hello, #{user.name}!"
end
end
Protocols define interfaces that multiple struct types can implement. Dispatch is based on the __struct__ key at runtime.
module Printable
protocol do
def to_s(value)
"unknown"
end
end
end
Use impl ProtocolName do ... end inside a struct module:
module User
struct [:name, :email]
impl Printable do
def to_s(user)
"User(#{user.name})"
end
end
end
module Post
struct [:title]
impl Printable do
def to_s(post)
"Post: #{post.title}"
end
end
end
Call the protocol function — dispatch happens automatically based on the struct type:
user = User.new(%{name: "Alice"})
post = Post.new(%{title: "Hello World"})
Printable.to_s(user) # => "User(Alice)"
Printable.to_s(post) # => "Post: Hello World"
Protocol implementations are registered at module load time. Multiple struct types can implement the same protocol.
Create anonymous functions with fn(params) => body end:
double = fn(x) => x * 2 end
double(5) # => 10
add = fn(a, b) => a + b end
add(3, 4) # => 7
constant = fn() => 42 end
constant() # => 42
Lambdas capture variables from their enclosing scope (closures):
def make_adder(n)
fn(x) => x + n end
end
add_ten = make_adder(10)
add_ten(5) # => 15
Iterate over a list and transform each element:
for x in [1, 2, 3] do
x * 10
end
# => [10, 20, 30]
Works with ranges:
for i in 1..5 do
i * i
end
# => [1, 4, 9, 16, 25]
Create a list of integers with ..:
1..5 # => [1, 2, 3, 4, 5]
1..1 # => [1]
Ranges work anywhere a list is expected:
1..10
|> Enum.filter() do |x| x > 5 end
|> Enum.map() do |x| x * 2 end
# => [12, 14, 16, 18, 20]
Access map fields with dot notation:
user = %{name: "Alice", age: 30}
user.name # => "Alice"
user.age # => 30
resp = HTTP.get("https://api.example.com/data")
resp.status # => 200
resp.body # => decoded JSON map
This is syntactic sugar for maps:get(field, map).
These are available as bare function calls (no module prefix):
to_string(42) # => "42"
to_string(:hello) # => "hello"
to_integer("123") # => 123
to_float(5) # => 5.0
to_atom("hello") # => :hello
inspect({:ok, 42}) # => "{ok,42}"
if/else is an expression — it returns a value.
if x > 0
:positive
else
:non_positive
end
else is optional:
if debug
IO.puts("debug mode")
end
Use as an expression:
label = if count > 100
"many"
else
"few"
end
Multi-branch matching on a value:
switch status
:active => "Active"
:inactive => "Inactive"
_ => "Unknown"
end
Switch clauses support any pattern — atoms, integers, tuples, wildcards:
switch code
200 => :ok
404 => :not_found
500 => :server_error
_ => :unknown
end
For multiple expressions in a clause body, just use newlines:
switch status
:active =>
Logger.info("user is active")
:ok
:inactive =>
Logger.warn("user inactive")
:disabled
_ => :unknown
end
The old do...end wrapper syntax also still works:
switch status
:active => do
Logger.info("user is active")
:ok
end
_ => :unknown
end
Use when to add conditions to function clauses and switch branches:
def divide(a, b) when b != 0
a / b
end
def divide(_, 0)
{:error, "division by zero"}
end
Guards on switch clauses:
switch value
n when n > 0 => :positive
n when n < 0 => :negative
_ => :zero
end
Multiple guarded clauses are matched top-to-bottom:
def grade(score) when score >= 90
:a
end
def grade(score) when score >= 80
:b
end
def grade(score) when score >= 70
:c
end
def grade(_)
:f
end
Handle exceptions with try/rescue:
try
risky_operation()
rescue
{:error, reason} => IO.puts("caught: " <> reason)
_ => IO.puts("unknown error")
end
try is an expression — the last evaluated value is returned:
result = try
dangerous_call()
rescue
_ => :fallback_value
end
Call functions on other modules with . notation:
IO.puts("Hello")
String.upcase(name)
Enum.map(list) do |x| x * 2 end
HTTP.get("https://api.example.com/data")
JWT.sign(%{user_id: 42}, secret)
Logger.info("request processed", %{duration_ms: 150})
Line comments start with #:
# This is a comment
def greet(name)
IO.puts("Hello, " <> name) # inline comment
end
Block comments use #| ... |# and can span multiple lines:
#|
This module handles user authentication.
It supports JWT and session-based auth.
|#
module Auth
def verify(token)
# ...
end
end
Block comments can also be used inline or to comment out code:
x = 42 #| temporary |# + 0