Skip to content
This repository has been archived by the owner on Mar 8, 2021. It is now read-only.
Brekk edited this page Nov 24, 2020 · 5 revisions

build Coverage Status

madlib

madlib is a general purpose language that compiles to Javascript.

Features and Ideology

madlib shares much of its syntax / ideology with JavaScript. Atop the "good stuff", it introduces functional programing concepts from other functional programming languages including:

  • algebraic data types
  • function composition
  • pattern matching

Type-checking

madlib does static type checking at compilation time using an approach based upon the Hindley-Milner W algorithm. (Type annotations are also possible, but mostly not needed.)

Variables

Variables in madlib may only be defined once and cannot be re-assigned. All data in madlib is immutable. Instead of picking between the different semantics / rules of var / let / const in JavaScript, in madlib you can eschew all three:

x = 3
y = x + 1
user = { name: "Max" }

Expressions

  1. Every expression in madlib must return a value.
  2. null / undefined are not valid keywords.
x + 1

Functions

Functions are the heart of madlib.

  1. A function is an expression which can be re-evaluated given contextual parameters.
  2. A function must always return a value.
  3. A function may only define a single expression.

Defining a function:

inc = (x) => x + 1

Typing a function:

inc :: Num -> Num
inc = (x) => x + 1

Evaluating a function

inc(3) // 4

Composing functions

We use the |> or pipeline operator to partially apply values or compose functions, left to right.

3 |> inc // 4
// equivalent to:
inc(3) // 4
3 |> inc |> inc // 5
// equivalent to
inc(inc(3)) // 5

Currying

All functions are curried, therefore you can always partially apply them:

add = (a, b) => a + b

addFive = add(5)

17 |> addFive // 22

Conditions

The keywords if / else are bound expressions in madlib and must return a value. The else case must be defined in order to be valid.

Some examples:

if (true) { "Yes" } else { "No" }
if (cost > wallet) { goHome() } else { watchShow() }

Because it is an expression, we can directly pipe to whatever it returns:

if (true) { "Yes" } else { "No" }
  |> IO.log

Type annotations

Because of madlib's type inference, in the majority of cases you do not need to provide type annotations. However, if needed, you can explicitly define type annotations in the form of (expression :: type):

(1 :: Num)     // here the annotation says that 1 is a Num
(1 + 1 :: Num) // here the annotation says that 1 + 1 is a Num
(1 :: Num) + 1 // here the annotation says that the first 1 is a Num, and tells the type checker to infer the type of the second value
("Madlib" :: String)
("Madlib" :: Bool) // Type error, "Madlib should be a Bool"

Algebraic Data Types

madlib allows for algebraic data types in the form of:

data Maybe a = Just a | Nothing

Here Maybe a is the type. This type has a variable, that means that a Maybe can have different shapes and contain any other type.

Just a and Nothing are constructors of the type Maybe. They allow us to create values with that type. data Maybe a = Just a | Nothing generates these constructor functions for us.

Here is the type above in use:

might = Just("something") // Maybe String
nope  = Nothing           // Maybe a

Pattern matching

Pattern matching is a powerful tool for specifying what to do in a given function or Record.

For functions:

greet = (greeting) => (
  where (greeting) {
    is "hello"   : "Hello right back at you!"
    is "morning" : "Good morning!"
    is "goodbye" : "Rude."
    is "..."     : "Rude NPC"
    is _         : "Hi there"
  }
)

data User
  = KnownUser String
  | Anonymous

userDisplayName = (u) => where(u) {
  is KnownUser name: name
  is Anonymous     : "Anonymous"
}

NB — the _ (underscore) character allows you to skip binding to a variable. In the case where it is used at the end of a where ... is _ pattern, it must be the final case, as it functions as an "otherwise", and will skip matching future is cases. So the following is invalid:

riggedGuessingGame :: Number -> Number -> String
riggedGuessingGame = (saved, given) => (
  where (given) {
    is _     : "You guessed wrong."
    is saved : "You guessed it!" // never runs
  }
)

For Records:

getStreetName :: { address: { street: String } }
getStreetName = (p1, p2) => where({ p1: p1, p2: p2 }) {
  is { address: { street: s } }: s
  is _                         : "Unknown address"
}

Records

madlib offers a special Record type. A Record is analogous to a JavaScript object. It is a syntax for defining a custom shape for your data. A Record's keys are identifiers and values can be any type. Here are examples:

language = { name: "Madlib", howIsIt: "cool" }

It can be used as constructor arguments by using Record types:

data User = LoggedIn { name :: String, age :: Num, address :: String }

It can be used in patterns:

user = LoggedIn({ name: "John", age: 33, address: "Street" })

where(us) {
  is LoggedIn { name: "John" }: "Hey John !!"
  is _                        : "It's not John"
}

As with JavaScript objects, records can be spread:

position2D = { x: 3, y: 7 }
position3D = { ...position2D, z: 1 }

Modules

In madlib your code is organized in modules.

  • A module is simply a source file.
  • A module can export functions or can import functions from other modules. To do this, a module can export any top level assignment.

Building Madlib

stack build

Running Madlib

stack build && stack exec -- madlib-exe -i examples/records.mad
node build/records.mjs