- Husky syntax
- Definitions
- Types
- Examples
- Functions
- Lambdas
- List comprehension
- Macros
- Special constructs
- Commands
- Gotchas
- Installation
- Author
- License
Husky is a lazy functional language similar to Haskell, but with a more conventional syntax. Husky implements a Hindley-Milner-style parametric polymorphic type system, higher-order functions, argument pattern matching for function specialization, currying, macros, list comprehension, and monads.
Husky uses NOR (normal order reduction), WHNF (weak head normal form), sharing (an important lazy evaluation optimization), and lazy constructors to create infinite "lazy" data structures.
For example, we can create an infinite list of integers, filter the even numbers, and only pick the first ten even numbers:
> take(10, filter(even, from(1))).
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20] :: [num].
Function from(1)
returns the infinite list [1, 2, 3, ...]
, even(n)
returns true
if n
is even, and filter
retains all even values in the list
using even
as a filter.
Husky is written in SWI-Prolog using Prolog unification for efficient beta
normal-order reduction of the lambda A -> B
applied to argument X
:
apply(A -> B, X, B) :- !, (var(A) -> A = X; eval(X, V) -> A = V).
The apply
operation takes a lambda A -> B
to apply to the unevaluated X
,
then returns B
when A
is a variable. If A
is not a variable, then
pattern matching is used by evaluating X
to V
and unifying A
with V
.
Sharing is implemented internally by using an eval(X, V)
functor that holds
an (un)evaluated expression X
and variable V
. When X
is evaluated to a
value, variable V
is set to that value, thereby sharing the value of V
to
all uses of V
(which are of the form of eval(X, V)
) in an expression.
This reduction method makes Husky run fast, asymptotically as fast as Haskell. Except that Husky does not optimize tail recursion, meaning that very deep recursive calls will eventually fail.
Type inference is performed by Prolog inference. Type rules are expressed as
Prolog clauses for the Husky type inference operator ::
. These clauses have
a one-to-one correspondence to Post system rules that are often used to define
polymorphic type systems with rules for type inference. For example, the
polymorphic type of a lambda is defined as a Prolog rule:
(A -> B) :: Alpha -> Beta :- A :: Alpha, B :: Beta.
This rule infers the type of a lambda as Alpha -> Beta
if argument A
is of
type Alpha
and B
is of type Beta
.
The type rule for lambda application (:
) uses Prolog unification with the
"occurs check" to safely implement the Hindley-Milner-style parametric
polymorphic type system of Husky:
F:A :: Beta :- !, A :: Alpha, F :: Gamma, unify_with_occurs_check(Gamma, (Alpha -> Beta))
It is that simple!
The Curry-Howard correspondence (the link between logic and computation) trivially follows by observing that the plain unoptimized beta reduction rule is the same as the type inference rule for application without the occurs check:
apply(F, A, B) :- eval(F, (A -> B)).
F:A :: Beta :- F :: (Alpha -> Beta), A :: Alpha.
In both rules A
and its type Alpha
are unified with the lambda argument to
produce B
and its type Beta
.
Because Husky evaluation rules and type inference rules are defined in Prolog, the implementation is easy to understand and change, for example to add features, to change the language, and to experiment with type systems.
Husky syntax follows the usual syntax of arithmetic expressions with functions. Functions are applied to one or more parenthesized arguments:
func(arg1, arg2, ...)
Values are Booleans true
and false
, numbers, strings (quoted with "
),
lists written with [
and ]
brackets, and arbitrary data structures.
At the Husky prompt you can enter expressions which are evaluated and the
answer is returned together with its inferred type. Global type declarations
and function definitions entered at the command prompt are stored with the
program. Input may span one or more lines and should end with a period (.
).
> 1+2.
3 :: num
> x^2 where x := sin(1)-2.
1.3421894790419853 :: num
> true /\ false \/ true.
true :: bool
> "hello" // "world".
"helloworld" :: string
> [1,2,3] ++ [4,5].
[1, 2, 3, 4, 5] :: [num]
> (1+2, \ true).
(3, false) :: (num, bool)
> x->x+1.
$0->$0+1 :: num->num
> f where f(x) := x+1.
$0->$0+1 :: num->num
> map(f, [1,2,3]) where f(x) := x+1.
[2,3,4] :: [num]
> zip([1, 2, 3], [true, true, false]).
[(1, true), (2, true), (3, false)] :: [(num, bool)]
Type variables and formal arguments of functions and lambdas are internally
anonymized as Prolog variables and displayed as $i
where i
is an integer.
Many functions and operators are predefined in Husky, either as built-ins or as prelude functions, see Functions.
In Husky, v->b
is a lambda (abstraction) with argument v
and body
expression b
. Applications of lambdas are written with a colon (:
), for
example (v->v+1):3
applies the lambda v->v+1
to the value 3
giving 3+1
which evaluates to 4
.
When applying named functions the usual parenthesized form can be used, for
example f(3)
to apply f
to the value 3
. Also f:3
is permitted.
Currying of functions and operators is supported. For example, addition can be
written in functional form +(x,y)
and (+):x:y
and even +(x):y
. These are
all different representations of the same expression. This makes Currying
intuitive, for example +(1)
is the increment function, *(2)
doubles, and
/(1)
inverts (note the parameter placement order in this case!).
Lists in Husky are written with brackets, for example[]
and [1,2]
. The
special form [x|xs]
represents a list with head expression x
and tail
expression xs
. The dot notation x.xs
represents a list with head x
and
tail xs
, where .
is the list constructor function commonly referred to as
"cons".
A definition in Husky is written as LHS := RHS
, where LHS
is a name or a
function name with parenthesized arguments and RHS
is an expression.
For example:
one := 1.
inc(x) := x + 1.
hypotenuse(x, y) := sqrt(x^2 + y^2).
Definitions of functions can span more than one LHS := RHS
pair in which the
definitions should be separated with a semicolon (;
). This allows you to
define pattern arguments for function specialization:
fact(0) := 1;
fact(n) := n * fact(n - 1).
Multiple definitions of a function typically have constants and data structures as arguments for which the function has a specialized function body.
A wildcard formal argument _
is permitted and represents an unused argument.
For example to define the const
combinator function:
const(x, _) := x.
Types are automatically inferred, like in Haskell. Types can also be
explicitly associated with definitions using the ::
operator. New named
types and parametric types can be defined.
Husky has three built-in atomic types:
bool Booleans
num integers, rationals, and floats
string strings
- the Boolean type has two values
true
andfalse
; - rationals are written with
rdiv
in the formnumerator rdiv denominator
; - floats are written conventionally with +/- sign, a period, and exponent
E
; - strings are sequences of characters enclosed in double quotes (
"
); - the
nil
constant denotes the so-called "bottom value" (an undefined value) and is polymorphic.
Husky has three built-in type constructors:
[alpha] a list of elements of type alpha
(alpha, beta) a tuple is the product of types alpha and beta
alpha -> beta a function mapping elements of type alpha to type beta
New (parametric) data types and their constructors can be defined as follows:
MyType :: type.
Constructor1(arg1, arg2, ...) :: MyType(parm1, parm2, ...).
...
ConstructorN(arg1, arg2, ...) :: MyType(parm1, parm2, ...).
A type variable is a name, except bool
, num
, and string
. Also symbols
such as *
and **
can be used as type variables. For example, a polymorphic
list type can be written as [alpha]
or [*]
and the comparison operator =
has type (=) :: * -> * -> bool
.
Some examples of named types and parametric types:
one :: num.
one := 1.
inc :: num->num.
inc := +(1).
Day :: type.
Mon :: Day.
Tue :: Day.
Wed :: Day.
Thu :: Day.
Fri :: Day.
Sat :: Day.
Sun :: Day.
WeekDays :: [Day].
WeekDays := [Mon,Tue,Wed,Thu,Fri].
isWeekDay(Mon) := true;
isWeekDay(Tue) := true;
isWeekDay(Wed) := true;
isWeekDay(Thu) := true;
isWeekDay(Fri) := true;
isWeekDay(day) := false.
verify := all(map(isWeekDay, WeekDays)).
BinTree :: type.
Leaf :: BinTree(*).
Node(*, BinTree(*), BinTree(*)) :: BinTree(*).
> a := 2.
{ DEFINED a::num }
> f(x) := x+1.
{ DEFINED f::num->num }
> f(a).
3 :: num
% the same function f, using the colon syntax for application:
> f:x := (+):x:1.
{ DEFINED f::num->num }
% the same function f, using currying:
> f := +(1).
{ DEFINED f::num->num }
% function definitions are translated to lambdas,
% typing the function name returns its lambda expression:
> f(a) := a+1.
{ DEFINED f::num->num }
> f.
$0->$0+1 :: num->num
> plus(x,y) := x+y.
{ DEFINED plus::num->num->num }
> plus.
$0->$1->$0+$1 :: num->num->num
% a lambda can be specialized by multiple abstractions, each
% separated by a ';', e.g. the factorial function:
> fact(0) := 1;
fact(n) := n*fact(n-1).
{ DEFINED fac::num->num }
> fact.
0->1;$0->$0*fac($0-1) :: num->num
> fact(6).
720 :: num
% constants can be defined to construct new data types
% declare a BinTree type:
> BinTree :: type.
{ DECLARED BinTree::type }
% define a Leaf constant:
> Leaf :: BinTree(*).
{ DECLARED Leaf::BinTree($0) }
% define a Node constructor:
> Node(*, BinTree(*), BinTree(*)) :: BinTree(*).
Node(num, BinTree(num), BinTree(num)) :: BinTree(num).
% try it out by building a small tree (see examples/btree.sky):
> Node(3, Node(1, Leaf, Leaf), Node(4, Leaf, Leaf)).
Node(3, Node(1, Leaf, Leaf), Node(4, Leaf, Leaf)) :: BinTree(num)
Built-in operators and functions are defined in prelude.sky:
- x unary minus (can also use 'neg' for unary minus, e.g. for currying)
x + y addition
x - y subtraction
x * y multiplication
x / y division
x rdiv y rational division with "infinite" precision
x div y integer division
x mod y modulo
x ^ y power
min(x,y) minimum
max(x,y) maximum
x = y equal (structural comparison)
x <> y not equal (structural comparison)
x < y less (num and string)
x <= y less or equal (num and string)
x > y greater (num and string)
x >= y greater or equal (num and string)
\ x logical not
x /\ y logical and
x \/ y logical or
gcd(x, y) GCD
lcm(x, y) LCM
fac(x) factorial
fib(x) Fibonacci
id(x) identity (returns x)
abs(x) absolute value
sin(x) sine
cos(x) cosine
tan(x) tangent
asin(x) arc sine
acos(x) arc cosine
atan(x) arc tangent
exp(x) e^x
log(x) natural logarithm (nil when x<0)
sqrt(x) root (nil when x<0)
x // y string concatenation
(x,y) tuple (note: tuple constructors are strict, not lazy)
x.xs a list with head x and tail xs (list constructors are lazy)
[x|xs] same as above: a list with head x and tail xs
# xs list length
xs ? n element of list xs at index n >= 1
hd(xs) head element of list xs (nil when x=[])
tl(xs) tail element of list xs (nil when x=[])
init(xs) init of xs without the last element
last(xs) last element of xs
xs ++ ys list concatenation
xs \\ ys list subtraction
x is_in xs list membership check (positive test)
x not_in xs list membership check (negative test)
fst((x,y)) first (=x)
snd((x,y)) second (=y)
even(x) true when x is even
odd(x) true when x is odd
num(s) convert string s to number (nil when s is not numeric)
str(x) convert number x to string
s2ascii(s) convert string s to list of ASCII codes
ascii2s(xs) convert list xs of ASCII codes to string
read prompts user and returns input string
write(x) display x (returns x)
writeln(x) display x '\n' (returns x)
getc returns single input char in ASCII
putc(x) display character of ASCII code x (returns x)
map map(f, xs) maps f onto xs [f(x1), f(x2), ..., f(xk)]
filter filter(p, xs) yields elements of xs that fulfill p
foldl foldl(f, x, xs) = f(x, ... f(f(f(x1, x2), x3), x4), ...)
foldr foldr(f, x, xs) = f(... f(xk-2, f(xk-1, f(xk, x))) ... )
foldl1 foldl1(f, xs) foldl over non-empty list
foldr1 foldr1(f, xs) foldr over non-empty list
scanl scanl left scan
scanr scanr right scan
all all(p, xs) = true if all elements in xs fulfill p
any any(p, xs) = true if any element in xs fulfills p
concat concat list of lists
zip zip(xs, ys) zips two lists
unzip unzip(xys) unzips list of tuples
zipwith zipwith(f, xs, ys) zips two lists using operator f
until until(p, f, x) applies f to x until p(x) holds
iterate iterate(f, x) = [x, f(x), f(f(x)), f(f(f(x))), ... ]
from from(n) = [n, n+1, n+2, ...]
repeat repeat(x) produces [x,x,x,...]
replicate replicate(n, x) generates list of n x's
cycle cycle(xs) generates cyclic list xs ++ xs ++ ...
drop drop(n, xs) drops n elements from xs
take take(n, xs) takes n elements from xs
dropwhile dropwhile(p, xs) drops first elements fulfilling p from xs
takewhile takewhile(p, xs) takes first elements fulfilling p from xs
reverse reverse list
f @ g function composition ("f after g")
twiddle twiddle operands twiddle(f:x, y) means f(y, x)
flip flip(f, x, y) flips operands to apply f(y, x)
curry curry function curry(f, x, y) := f((x, y))
uncurry uncurry function uncurry(f, (x, y)) := f(x, y)
A lamba (abstraction) is written as v->b
. A lambda expression may have
several lambda alternatives for function specialization. Alternatives are
represented by lambdas separated by semicolons (;
). Each lambda should
specify a specialized argument for selection, such as constants and partial
data structures.
(v->b) a lambda abstraction
(v->b; w->c) a lambda with two specializations
Argument v
and w
may be a name, a constant, or a constructor with named
parameters. The body of a lambda is just an expression.
Lambdas should be parenthesized when used in expressions, because the ->
and ;
operators have a low precedence.
Function application is performed with the conventional syntax of argument parameterization:
> f := (x->x+1).
{ DEFINED f::num->num }
> f(3).
4 :: num
> iszero := (0->true; n->false).
{ DEFINED iszero::num->bool }
However, applying a lambda directly to an argument requires the :
application
operator:
> (x->x+1):3.
4 :: num
Functions and functions with specializations are automatically translated to the corresponding lambda abstractions when stored with the program:
> f(x) := x+1.
{ DEFINED f::num->num }
> f.
($0->$0+1) :: (num->num)
> iszero(0) := true;
|: iszero(n) := false.
{ DEFINED iszero::num->bool }
> iszero.
(0->true;$0->false) :: (num->bool)
A list comprehension has the following form:
[ expr | qual; qual; ...; qual ]
where the qualifiers are generators of the form x <- xs
or guards over the
free variables. Guards over one or more generator variables must be placed to
the right of the generator, i.e. variable scoping applies to the right.
For example:
[ x^2 | x <- 1..10; even(x) ].
[ (a,b) | a <- 1..3; b <- 1..a ].
[ (a,b) | a <- 1..5; odd(a); b <- 1..a; a mod b = 0 ].
Macros are defined with ==
to serve as shorthands for expressions and types:
StartValue == 0.
Address == num.
MachineState == Address->num.
NumTree == BinTree(num).
Macro expansion is structural: names, functions, lists, and other syntactic structures can be defined as macros with parameters, i.e. macros support pattern matching for macro specialization. Parameter names in macros are unified (viz. Prolog unification) to structurally match macro definitions without evaluating the arguments.
For example:
> macro(x, x) == "equal".
> macro(x, y) == "not equal".
> macro(1, 1).
"equal" :: string
> macro(1, 2).
"not equal" :: string
> macro(1, 1+0).
"not equal" :: string
In fact, list comprehension syntax is entirely implemented by recursive macro
expansion into concat
, map
, and filter
(defined in prelude.sky):
[ f | x <- xs; p; y <- ys; r ] == concat(map(x -> [ f | y <- ys; r ], filter(x -> p, xs))).
[ f | x <- xs; p; y <- ys ] == concat(map(x -> [ f | y <- ys ], filter(x -> p, xs))).
[ f | x <- xs; y <- ys; r ] == concat(map(x -> [ f | y <- ys; r ], xs)).
[ f | x <- xs; y <- ys ] == concat(map(x -> [ f | y <- ys ], xs)).
[ f | x <- xs; p; q; r ] == [ f | x <- xs; p /\ q; r ].
[ f | x <- xs; p; q ] == [ f | x <- xs; p /\ q ].
[ x | x <- xs; p ] == filter(x -> p, xs).
[ f | x <- xs; p ] == map(x -> f, filter(x -> p, xs)).
[ x | x <- xs ] == xs.
[ f | x <- xs ] == map(x -> f, xs).
The monad do
operator (defined in monads.sky) is a macro:
do(x := y; z) == do(z) where x := y.
do(x <- y; z) == y >>= (x -> do(z)).
do(z) == z.
Macros do not evaluate arguments! Only syntactical structures can be pattern matched. New syntax can be introduced by declaring prefix, infix, and postfix operators, see Commands.
if b then x else y a conditional expression
x where y := z each y in expression x is replaced by expression z
fix(f) the fixed-point Y-combinator (fix:f = f:(fix:f))
(let defs in val) the 'let' construct (use parenthesis!)
case val of (const1 -> val1; const2 -> val2; ... constk -> valk)
the 'case' construct (translated to lambdas)
For example:
test := (let f := ^(2); start := 1; end := 10 in map(f, start..end)).
case n of (1 -> "one"; 2 -> "two"; 3 -> "three")
listing. lists the contents of the loaded program(s)
bye. close session
reset. soft reset
trace. trace on
notrace. trace off
profile. collect execution profile stats
noprofile. profile off
save. create standalone Husky executable
load "file". load file
save "file". save listing to file
:- Goal. execute Prolog Goal
remove name. remove name from definitions
prefix (op). declare prefix operator op
postfix (op). declare postfix operator op
infix (op). declare non-associative infix operator op
infixl (op). declare left associative infix operator op
infixr (op). declare right associative infix operator op
num prefix (op). declare prefix operator op with precedence num
num postfix (op). declare postfix operator op with precedence num
num infix (op). declare non-associative infix operator op w. prec. num
num infixl (op). declare left associative infix operator op w. prec. num
num infixr (op). declare right associative infix operator op w. prec. num
-
An expression or command must be terminated by a period (
.
). -
There cannot be a space after the list cons dot (
.
) operator inx.xs
, otherwise the system considers the input line has ended. As an alternative use[x|xs]
, for example ifxs
is a compound expression. -
There must be a space between different operators, otherwise the system is not able to separate the operators and parse the expression. For example:
x\/\y should be written x\/ \y x+#l should be written x+ #l x*-y should be written x* -y x<-y should be written x< -y
-
Parenthesization follows the Prolog syntax, so
f(x,y)
works, butf(x)(y)
causes a syntax error. Instead, use colons, e.g.f:x:y
or a mixed form such asf(x):y
. -
The scope of the
where
construct is limited to its left-hand side. Therefore, it does not support recursive definitions:BAD: ... f(x) ... where f(n) := ... f(n-1) ...
use the
fix
operator instead:GOOD: ... fix(f, x) ... where f(ff, n) := ... ff(n-1) ...
-
When using tuples in
where
, be aware that tuple constructors are strict:x+1 where (x,y) := (1,write("abc");
which displays "abc" even though
y
is not used, because tuple members are evaluated. -
Patterns in function arguments may consist of (parameterized) constants, empty list
[]
,x.xs
non-empty list with headx
and tailxs
, and tuples(x,y)
. These should not be nested:BAD: f((x,y).xs) := ... GOOD: f(p.xs) := ... where (x,y) := p. BAD: f(BinTree(BinTree(left, right), rest)) := ... GOOD: f(BinTree(tree, rest)) := ... where left := f(tree) where right := g(tree) where f(BinTree(l, r)) := l where g(BinTree(l, r)) := r.
Install SWI-Prolog 8.2.1 or greater from https://www.swi-prolog.org.
Then clone Husky with git:
$ git clone https://github.com/Genivia/husky
Run swipl
then load Husky and run the husky
interpreter:
$ swipl
?- [husky].
:- husky.
After starting Husky, load any one of the example .sky files with:
> load "examples/<filename>".
To delete programs from memory and reset Husky to system defaults:
> reset.
To create a stand-alone executable, run swipl
and enter the following
commands:
$ swipl
:- [husky].
:- husky.
> save.
> bye.
This creates the `Husky' executable with the prelude functions preloaded. You can also load one or more programs and then save the executable with the programs included.
Husky source files:
README.md this file
husky.pl the Husky interpreter
prelude.sky built-in definitions
Husky program examples:
palindrome.sky palindrome
btree.sky binary trees
clientserver.sky client-server stream example
combinators.sky SKI combinator calculus
cr.sky Chains of Recurrences algebra
hamming.sky the Hamming problem
hangman.sky a simple hangman game
matrix.sky matrix and vector operations
monads.sky monads a-la Haskell
primes.sky primes
qsort.sky quicksort
queens.sky the queens problem
semantics.sky denotational semantics example
Husky is created by Prof. Robert A. van Engelen, engelen@acm.org.
Open source GPL (GNU public license).