madlib is a general purpose language that compiles to native binaries and Javascript.
There are currently two ways to install the madlib compiler.
You can install it globally with the command npm i -g @madlib-lang/madlib
. You can then compile code in the following way:
madlib -i "entryFile.mad" -o "outputFolder"
You can also download the archive of the build directly from the releases releases and install it wherever you want in your filesystem. You would then need to make sure that the location is in your PATH environment variable to make it available from everywhere.
madlib shares much of its syntax / ideology with JavaScript. Atop the "good stuff", it introduces functional programing concepts from other functional programming languages including:
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 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" }
- Every expression in madlib must return a value.
null
/undefined
are not valid keywords.
x + 1
1
"Hello world"
// else is mandatory as the expression must return a value
if (a % 2 == 0) { "even" } else { "odd" }
where(maybe) {
Just(x) =>
x
Nothing =>
"Hello world"
}
Functions are the heart of madlib.
- A function is an expression which can be re-evaluated given contextual parameters.
- A function must always return a value.
- A function may only define a single expression.
inc = (x) => x + 1
inc :: Integer -> Integer
inc = (x) => x + 1
inc(3) // 4
The special pipe expression, which returns a function.
compute :: Float -> Float
compute = pipe(
inc,
add(10),
divide(2)
)
Alternatively you can find use the |>
or pipeline operator to partially apply values or compose functions, left to right.
3 |> inc
// equivalent to:
inc(3)
3 |> inc |> inc
// equivalent to
inc(inc(3))
All functions are curried, therefore you can always partially apply them:
add = (a, b) => a + b
addFive = add(5)
17 |> addFive // 22
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("With my little money") } else { watchShow("And enjoy it") }
Because it is an expression, we can directly pipe to whatever it returns:
if (true) { "Yes" } else { "No" }
|> IO.log
Two shorthand syntaxes are also available and the above could then also be written like this:
// Without brackets:
(if (true) "Yes" else "No")
|> IO.log
// Ternary:
(true ? "Yes" : "No")
|> IO.log
NB: note that parenthesis are necessary around the expressions then, otherwise the piped IO.Log
would be applied to the "No" string.
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 :: Integer) // here the annotation says that 1 is a Integer
(1 + 1 :: Integer) // here the annotation says that 1 + 1 is a Integer
(1 :: Integer) + 1 // here the annotation says that the first 1 is a Integer, and tells the type checker to infer the type of the second value
("Madlib" :: String)
("Madlib" :: Boolean) // Type error, "Madlib" should be a Boolean
madlib allows for algebraic data types in the form of:
type 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. type 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 is a powerful tool for specifying what to do in a given function or Record.
For functions:
type User
= LoggedIn(String)
| Anonymous
userDisplayName = (u) => where(u) {
LoggedIn(name) =>
name
Anonymous =>
"Anonymous"
}
For Records:
getStreetName :: { address :: { street :: String } } -> String
getStreetName = (profile) => where(profile) {
{ address: { street } } =>
street
_ =>
"Unknown address"
}
Note that you can use where without parameter, in which case it returns a function that takes whatever is matched as a parameter. So the above can be shortened like this:
getStreetName :: { address :: { street :: String } } -> String
getStreetName = where {
{ address: { street } } =>
street
_ =>
"Unknown address"
}
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:
type User = LoggedIn({ name :: String, age :: Integer, address :: String })
It can be used in patterns:
user = LoggedIn({ name: "John", age: 33, address: "Street" })
where(user) {
LoggedIn({ name: "John" }) =>
"Hey John !!"
_ =>
"It's not John"
}
Records can be updated:
position = { x: 3, y: 7, z: -1 }
updatedPosition = { ...position, z: 1 }
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.
Right now the entrypoint module that you give to the compiler is the reference and its path defines the root path for your modules.
Given the following structure:
src/Main.mad
/Dependency.mad
// Dependency.mad
export someFn = (a) => ...
// Main.mad
import { someFn } from "./Dependency"
someFn(...)
Subfolders can be used to group related modules together. If we add one, it ends up with the current structure:
src/Main.mad
/Dependency.mad
/Sub/SubDependency.mad
Then we could have the modules defined like this:
// Sub/SubDependency.mad
export someSubFn = (a) => ...
// Main.mad
import { someSubFn } from "./Sub/SubDependency"
someSubFn(...)
All exported names are automatically added to a default export that can then be imported as a default import in order to avoid naming collisions. For example, when importing map
from the standard List module, you can do it like this:
import List from 'List'
List.map((x) => (x * 2), [1, 2, 3])
A package is a way to share code across projects or create libraries. A package must have a madlib.json file with a following structure:
{
"name": "MadUI",
"version": "0.0.1",
"madlibVersion": "0.11.0", // the minimum madlib version required by the project
"main": "src/Main.mad", // you must define the main module of your package
"dependencies": [ // dependencies of your package
{
"url": "http://some.dep.url.zip",
"description": "...",
"minVersion": "0.1.0",
"maxVersion": "2.0.7"
}
]
}
The main module must export every name that you want to share.
import IO from "IO"
IO.putLine("Hello World !")
stack build
./scripts/run run "examples/HelloWorld.mad"
node build/HelloWorld.mjs
Your system should be setup with the following:
- Stack and GHC
8.10.7
- Node.js ( > v14 is recommended )
- Rollup with the package
@rollup/plugin-node-resolve
installed globally if you want to enable bundling or use the build script located at/scripts/build
This script builds a local version of madlib as well as dependencies that are built in Madlib itself. Currently these are located in tools and are composed of the test runner and the package downloader.
To run it:
./scripts/build
This runs a npm global install ( npm link
effectively ) of the local package. Beware that the version used
in the package.json there needs to be a github release that is published in this
repository. You can have a look here to find out which versions are available:
https://github.com/madlib-lang/madlib/releases.
To run it:
./scripts/install
This script allows you to swap the Madlib binary, Prelude Madlib files, and the tools
with the currently compiled version ( using the build
script ). Then the latest
local build becomes globally available if you rely on it for other local projects
outside the directory of Madlib itself.
To run it:
./scripts/update-pkg-build
A wrapper to stack run, the usage is the same as Madlib executable itself but uses the locally build one.
To run it:
./scripts/run --help
./scripts/run compile -i INPUT [OPTIONS]