milk-lang

0.1.18 • Public • Published

Milk npm version

Milk is a language that compiles into JavaScript. It is the love child of Python and JavaScript.

Installation

npm install -g milk-lang

To get started with Milk, read the manual of the Milk compiler:

milkc -h

Philosophy

Milk tries to be as readable and beautiful as Python without sacrificing the typical JavaScript flavor. Thus keywords and operators from JavaScript hasn’t been changed. One can even use a JavaScript syntax highlighter on Milk source code. Check out the examples to get a feeling of the language.

Language Reference

Just like Python, Milk uses significant whitespace to delimit blocks of code. Semicolons are not necessary to terminate statements, however they can be used to fit several statements onto a single line. A statement is usually terminated by a line ending, but if an expression is too long, a line break at any binary operator such as +, * or . won’t terminate it.

Contents

Functions

There are several ways to define a function in Milk. That’s the standard way to do it:

function square(x):
    r = x * x
    return r

Unlike Python you actually can pass such a function as a function argument:

list = [1, 2, 3, 4, 5]
 
list.map(function(x):
    return x * x
)

There is also the lambda notation, a shorthand notation for functions which return an expression immediately:

list.map(x => x * x)
 
cube = x => square(x) * x
exp = (x, n) => n == 0 ? 1 : x * exp(x, n - 1)
moduloSum = (x, y, n) => (% n + y % n) % n

If you already have a function, you can create a curried function using placeholders _ like this:

square = exp(_, 2)
cube = exp(_, 3)
 
mod5Sum = moduloSum(_, _, 5)
// mod5Sum(4, 8) == 2

That’s why you should avoid using the underscore as a variable name. Technically, _ is a keyword, but if Milk can’t make sense of _, it’s converted to a normal identifier in JavaScript.

You can use the spread operator * if you want to pass an array as arguments to a function call:

items = [6, 7, 8, 9, 10]
list.push(*items, 11, 12)
 
// This is equivalent to:
list.push(6, 7, 8, 9, 10, 11, 12)

Arrays and Objects

The syntax for arrays and objects is nearly identical to those in JavaScript. Commas are optional if the items are separated by a line break.

workdays = [
    "Monday", "Tuesday", "Wednesday"
    "Thursday", "Friday"
]
 
magic = {
    anima: {
        fire: ['Fire', 'Elfire', 'Meteor', 'Bolganone', 'Forblaze']
        wind: ['Wind', 'Elwind', 'Tornado', 'Blizzard', 'Fimbulvetr', 'Excalibur']
        thunder: ['Thunder', 'Elthunder', 'Thoron', 'Bolting', 'Mjölnir']
    }
    light: ['Lightning', 'Shine', 'Divine', 'Purge', 'Aura']
    dark: ['Flux', 'Fenrir', 'Eclipse', 'Nosferatu', 'Luna']
}

Milk can flatten arrays inside arrays using the spread operator *:

days = ['Sunday', *workdays, 'Saturday']
// days == ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]

There is no range function, but there is the following Ruby-like shorthand notation. Unlike Ruby, there is only one range construct and it works the same everywhere.

stop = 875
range = [0...stop]
// range == [0, 1, 2, 3, ..., 875]
 
countdown = [10...1]
// countdown == [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
 
even = [0, 2, ..., stop]
// even == [0, 2, 4, 6, 8, ..., 874]
odd = [1, 3, ..., stop]
// odd == [1, 3, 5, 7, 9, ..., 875]

There are array and object comprehensions just like in Python:

squares = [* x for x in [1, ..., 10]]
// squares == [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
 
nodiag = [[x, y] for x in [1, 2, 3] for y in [1, 2, 3] if x != y]
// nodiag == [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]
 
goboard = [['empty' for x in [1...19]] for y in [1...19]]
// goboard == [
//     ['empty', 'empty', ..., 'empty']
//     ['empty', 'empty', ..., 'empty']
//     ...
//     ['empty', 'empty', ..., 'empty']
// ]
 
protagonists = ['Ryan', 'Cyn', 'Pat', 'Lio']
dict = {name: name.length for name in protagonists if name != 'Lio'}
// dict == {
//     "Ryan": 4
//     "Cyn": 3
//     "Pat": 3
// }

Lexical Scoping

You don’t need to declare variables with the var keyword. Milk makes sure that all of your variables are declared within lexical scope. The behavior is identical to CoffeeScript or Ruby; it’s impossible to shadow an outer variable on purpose.

Like CoffeeScript, all Milk output is wrapped in a safety wrapper. This makes it difficult to pollute the global namespace.

Control Flow

If statements work just like in Python, except there is no elif keyword and expressions look like JavaScript.

age = 16
 
if age < 6:
    console.log('You are way too young for this.')
else if age < 12:
    console.log('You need adult supervision. Only anima and light magic allowed.')
else if age < 18:
    console.log('You can do it on your own, but no dark magic.')
else:
    console.log('Do what you want, but adhere to the Laws.')

Milk supports chained comparisons à la Python. Each expression is evaluated at most once:

if 12 <= age < 18 <= getSupervisorAge() || 18 <= age:
    console.log('You can do dark magic.')

There are no switch or with statements. Try statements do not require a catch or finally clause and the error parameter is optional:

try:
    // Fire
    invoke(magic.anima.fire[0])
catch e:
    // What now?
    console.error(e)
finally:
    clean()

While loops are also straightforward:

while age >= 18 && !isWorking():
    // Nosferatu
    invoke(magic.dark[2])

For loops work similar to those in Python. Like in array comprehensions, you can specify a filtering condition with if:

for day in days if day != 'Monday':
    console.log(day)
// =>
// Sunday
// Tuesday
// Wednesday
// ...

If you loop over objects, Milk will loop over the object keys:

for key in magic:
    console.log(magic)
// =>
// anima
// light
// dark

If you specify two variables, the first one will assume the index or key respectively, and the second one will assume the value:

for i, day in days if i >= 1:
    console.log(i, day)
// =>
// 1 Monday
// 2 Tuesday
// 3 Wednesday
// ...
 
for type, list in magic.anima:
    console.log(type, list.length)
// =>
// fire 5
// wind 6
// thunder 5

These looping techniques can be used in an array/object comprehension.

Pattern Matching

Milk has destructuring assignment syntax like JavaScript:

tome1 = 'General Compositional Theory of Anima Magic, Volume Eight'
tome2 = 'Elder Summoning Theory'
 
[tome1, tome2] = [tome2, tome1]
 
index2grid = (i, width) => [% width, Math.floor(/ width)]
index2goboard = index2grid(_, 19)
[x, y] = index2goboard(243)

Pattern matching can be used with any depth of array and object nesting. Use the placeholder keyword if you don’t want to assign the corresponding value.

{anima: {wind: [a, _, b, _, _, _]}, dark} = magic
 
// Tornado
invoke(b)
 
console.log(dark)
// => ["Flux", "Fenrir", "Eclipse", "Nosferatu", "Luna"]

Use the spread operator * to extract a sublist of items:

{anima: {wind: [wind, *rest, excalibur]}} = magic
// rest == ["Elwind", "Tornado", "Blizzard", "Fimbulvetr"]

You can use ... instead of *_. It is possible to assign default values to variables, should the extraction be null or undefined:

[first, second, third = 'Blah'] = [tome1, tome2]
// third == "Blah"

You can use patterns nearly everywhere where you have to define variables. In fact, function arguments is just another array pattern:

function addIngredients(first = 'Milk', second, *rest):
    console.log(first)
    console.log(second)
    console.log(rest)
 
addIngredients(null, 'Coffee beans', 'Python skin')
// =>
// Milk
// Coffee beans
// ["Python skin"]

Pattern matching work in for loops:

nodiag = [[x, y] for x in [1, 2, 3] for y in [1, 2, 3] if x != y]
// nodiag == [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]
 
for i, [x, y] in nodiag:
    console.log(i, x + y)
// =>
// 0 3
// 1 4
// 2 3
// 3 5
// 4 4
// 5 5

And even in catch clauses:

try:
    // Thunder
    invoke(magic.anima.thunder[0])
catch {message, location = getCurrentLocation()}:
    // Probably no thunder
    console.log(message)
finally:
    clean()

Operators

Apart from the usual arithmetic operators +, -, *, /, Milk has a right associative exponential operator ^. Milk’s modulo operator % always returns a positive number.

Milk compiles == and != into === and !== respectively, unless the right hand side is null. Milk has an equals operator for deep comparison between two objects and a not equals operator for its negation:

tuple = ['Wind', 'Excalibur']
clone = ['Wind', 'Excalibur']
 
console.log(clone equals tuple)
// => true

The in operator works differently in Milk. If the right hand side is an array or a string, it will test for collection membership. Otherwise, it will behave as the vanilla JavaScript in operator. You can use not in for its negation.

'Tornado' in magic.anima.wind
// true
'Tornado' in magic.anima
// false
'wind' in magic.anima
// true

For function composition you can use the @ operator:

// Flux
(console.log @ invoke)(magic.dark[0])
 
// This is equivalent to:
console.log(invoke(magic.dark[0]))

The existential operator ?? is syntactic sugar for a null or undefined check:

context = module ?? window
 
// This is equivalent to:
context = typeof module == 'undefined' || module == null ? window : module

You can soak up null references in a chain of function calls, membership accesses and properties using the soak operators ?(), ?[] and ?.. If all properties and calls exist, you’ll get the expected result. If the chain is broken, null is returned.

for i, item in list:
    prev = list[- 1]?.getValue?(item)?[1]
    next = list[+ 1]?.getValue?(item)?[1]
 
    if prev == next:
        console.log(item)

Classes and Inheritance

Milk provides syntactical sugar for creating classes. The constructor is called init:

class Graph:
    function init(vertices, edges):
        self._adjacencyList = {v: [] for v in vertices}
 
        for [v1, v2] in edges:
            self.addEdge(v1, v2)
 
    function getNeighbors(v):
        return self._adjacencyList[v].slice(0)
 
    function addEdge(v1, v2):
        self._adjacencyList[v1].push(v2)
        self._adjacencyList[v2].push(v1)
 
    function hasEdge(v1, v2):
        return v2 in self._adjacencyList[v1]
 
vertices = [1...10]
edges = [[v, w] for v in vertices for w in vertices if Math.abs(v - w) == 1]
graph = new Graph(vertices, edges)
 
console.log(graph.getNeighbors(5))
// => [4, 6]

Note that anything other than functions in a class block will be ignored. In each class function there is a self variable referencing the current instance, so you don’t have to worry about binding. There is syntactic sugar for inheriting as well:

class Labyrinth extends Graph:
    function init(width, height):
        vertices = [[x, y] for x in [1...width] for y in [1...height]]
        edges = [[v, w] for v in vertices for w in vertices if self.distance(v, w) == 1]
 
        super.init(vertices, edges)
 
    function distance([x1, y1], [x2, y2]):
        return Math.abs(x1 - x2) + Math.abs(y1 - y2)
 
labyrinth = new Labyrinth(10, 5)
console.log(labyrinth.getNeighbors([3, 3]))
// => [[2, 3], [3, 2], [3, 4], [4, 3]]

To call a function from the immediate ancestor, you can use the super keyword.

Package Sidebar

Install

npm i milk-lang

Weekly Downloads

4

Version

0.1.18

License

MIT

Last publish

Collaborators

  • yishn