Skip to content

i2y/mochi

Repository files navigation

Mochi

Mochi is a dynamically typed programming language for functional programming and actor-style programming.

Its interpreter is written in Python3. The interpreter translates a program written in Mochi to Python3's AST / bytecode.

Features

  • Python-like syntax
  • Tail recursion optimization (self tail recursion only), and no loop syntax
  • Re-assignments are not allowed in function definition.
  • Basic collection type is a persistent data structure. (using Pyrsistent)
  • Pattern matching / Data types, like algebraic data types
  • Pipeline operator
  • Syntax sugar of anonymous function definition
  • Actor, like the actor of Erlang (using Eventlet)
  • Macro, like the traditional macro of Lisp
  • Builtin functions includes functions exported by itertools module, recipes, functools module and operator module

Examples

Factorial

def factorial(n, m):
    if n == 1:
        m
    else:
        factorial(n - 1, n * m)


factorial(10000, 1)
# => 28462596809170545189064132121198688...

# Or

def factorial:
    n: factorial(n, 1)
    0, acc: acc
    n, acc: factorial(n - 1, acc * n)
    
factorial(10000)
# => 28462596809170545189064132121198688...

FizzBuzz

def fizzbuzz(n):
    match [n % 3, n % 5]:
        [0, 0]: "fizzbuzz"
        [0, _]: "fizz"
        [_, 0]: "buzz"
        _: n

range(1, 31)
|> map(fizzbuzz)
|> pvector()
|> print()

Actor

def show():
    receive:
        message:
            print(message)
            show()

actor = spawn(show)

send('foo', actor)
actor ! 'bar' # send('bar', actor)

sleep(1)
# -> foo
# -> bar


'foo' !> spawn(show)

sleep(1)
# -> foo

['foo', 'bar'] !&> spawn(show)
# The meaning of the above is the same as the meaning of the following.
# spawn(show) ! 'foo'
# spawn(show) ! 'bar'

sleep(1)
# -> foo
# -> bar

def show_loop():
    receive:
        [tag, value]:
            print(tag, value)
            show_loop()

actor2 = spawn(show_loop)

actor2 ! ["bar", 2000]
sleep(1)
# -> bar 2000

['foo', 1000] !> spawn(show_loop)
sleep(1)
# -> foo 1000

[['foo', 1000],['bar', 2000]] !&> spawn(show_loop)
sleep(1)
# -> foo 1000
# -> bar 2000

Distributed Computing

# comsumer.mochi
from mochi.actor.mailbox import KombuMailbox, ZmqInbox, SQSMailbox

def consumer():
    receive:
        'exit':
            print('exit!')
        other:
            print(other)
            consumer()

kombu_mailbox = KombuMailbox('sqs://<access_key_id>@<secret_access_key>:80//',
                             '<queue_name>',
                             dict(region='<region>'))
spawn_with_mailbox(consumer, kombu_mailbox)

zmq_mailbox = ZmqInbox('tcp://*:9999')
spawn_with_mailbox(consumer, zmq_mailbox)

sqs_mailbox = SQSMailbox('<queue_name>')
spawn_with_mailbox(consumer, sqs_mailbox)

wait_all()
# producer.mochi
from mochi.actor.mailbox import KombuMailbox, ZmqOutbox, SQSMailbox

kombu_mailbox = KombuMailbox('sqs://<access_key_id>@<secret_access_key>:80//',
                             '<queue_name>',
                             dict(region='<region>'))
kombu_mailbox ! [1, 2, 3]
kombu_mailbox ! 'exit'

zmq_mailbox = ZmqOutbox('tcp://localhost:9999')
zmq_mailbox ! [4, 5, 6]
zmq_mailbox ! 'exit'

sqs_mailbox = SQSMailbox('<queue_name>')
sqs_mailbox ! [7, 8, 9]
sqs_mailbox ! 'exit'

Flask

from flask import Flask

app = Flask('demo')

@app.route('/')
def hello():
    'Hello World!'

app.run()

RxPY

# usage: mochi -no-mp timer.mochi
# original:
# https://github.com/ReactiveX/RxPY/blob/master/examples/parallel/timer.py

import rx
import concurrent.futures
import time

seconds = [5, 1, 2, 4, 3]


def sleep(t):
    time.sleep(t)
    return t


def output(result):
    print('%d seconds' % result)


with concurrent.futures.ProcessPoolExecutor(5) as executor:
    rx.Observable.from_(seconds)
                 .flat_map((s) -> executor.submit(sleep, s))
                 .subscribe(output)

# 1 seconds
# 2 seconds
# 3 seconds
# 4 seconds
# 5 seconds

aif (Anaphoric macro)

macro aif(test, true_expr, false_expr):
    quasi_quote:
        it = unquote(test)
        if it:
            unquote(true_expr)
        else:
            unquote(false_expr)

aif([], first(it), "empty")
# => "empty"
aif([10, 20], first(it), "empty")
# => 10

Requirements

See requirements.txt

Installation

$ pip3 install mochi

Optional Installation

$ pip3 install flask Flask-RESTful Pillow RxPY  # to run the examples
$ pip3 install kombu # to use KombuMailbox
$ pip3 install boto # to use SQS as transport of KombuMailbox
$ pip3 install boto3 # to use SQSMailbox

Th error of the following may occur when you run Mochi on PyPy.

ImportError: Importing zmq.backend.cffi failed with version mismatch, 0.8.2 != 0.9.2

In this case, please change the version of cffi to 0.8.2 using pip on PyPy.

$ pip3 uninstall cffi
$ pip3 install cffi==0.8.2

Usage

REPL

$ mochi
>>>

loading and running a file

$ cat kinako.mochi
print('kinako')
$ mochi kinako.mochi
kinako
$ mochi -no-mp kinako.mochi  # not apply eventlet's monkey patching
kinako

byte compilation

$ mochi -c kinako.mochi > kinako.mochic

running a byte-compiled file

$ mochi -e kinako.mochic
kinako
$ mochi -e -no-mp kinako.mochic  # not apply eventlet's monkey patching
kinako

generating .pyc

$ ls
kagami.mochi
$ cat kagami.mochi
print('kagami')
$ mochi
>>> import kagami
kagami
>>> exit()
$ ls
kagami.mochi kagami.pyc
$ python3 kagami.pyc
kagami

Or

$ mochi -pyc kagami.mochi > kagami.pyc
$ python3 kagami.pyc
kagami
$ mochi -pyc -no-mp kagami.mochi > kagami.pyc  # not apply eventlet's monkey patching
$ python3 kagami.pyc
kagami

Examples for each feature

Persistent data structures

[1, 2, 3]
# => pvector([1, 2, 3])

v(1, 2, 3)
# => pvector([1, 2, 3])

vec = [1, 2, 3]
vec2 = vec.set(0, 8)
# => pvector([8, 2, 3]
vec
# => pvector([1, 2, 3])
[x, y, z] = vec
x # => 1
y # => 2
z # => 3

get(vec, 0) # => 1
get(vec, 0, 2) # => [1, 2]

vec[0] # => 1
vec[0:2] # => [1, 2]

{'x': 100, 'y': 200}
# => pmap({'y': 200, 'x': 100})

ma = {'x': 100, 'y': 200}
ma.get('x') # => 100
ma.x # => 100
ma['x'] # => 100
ma2 = ma.set('x', 10000)
# => pmap({'y': 200, 'x': 10000})
ma # => pmap({'y': 200, 'x': 100})
get(ma, 'y') # => 200
ma['y'] # => 200

m(x=100, y=200)
# => pmap({'y': 200, 'x': 100})

s(1, 2, 3)
# => pset([1, 2, 3])

b(1, 2, 3)
# => pbag([1, 2, 3])

Function definitions

def hoge(x):
    'hoge' + str(x)

hoge(3)
# => hoge3

Pattern matching

lis = [1, 2, 3]

# Sequence pattern
match lis:
    [1, 2, x]: x
    _: None
# => 3

match lis:
    [1, &rest]: rest
    _: None

# => pvector([2, 3])


foo_map = {'foo' : 'bar'}

# Mapping pattern
match foo_map:
    {'foo' : value}: value
    _: None
# => 'bar'


# Type pattern
# <name of variable refers to type> <pattern>: <action>
match 10:
    int x: 'int'
    float x: 'float'
    str x: 'str'
    bool x: 'bool'
    _: 'other'
# => 'int'

match [1, 2, 3]:
    [1, str x, 3]: 'str'
    [1, int x, 3]: 'int'
    _: 'other'
# => 'int'

num = union(int, float)
vector nums[num]
vector strs[str]

match nums([1, 2, 3]):
    nums[x, y, z]: z
    strs[x, y, z]: x
# => 3

Positive = predicate(-> $1 > 0)
Even = predicate(-> $1 % 2 == 0)
EvenAndPositive = predicate(-> ($1 % 2 == 0) and ($1 >= 0)) 

match 10:
    EvenAndPositive n: str(n) + ':Even and Positive'
    Even n: str(n) + ':Even'
    Positive n: str(n) + ':Positive'

# => 10:Even and Positive


# Or pattern
match ['foo', 100]:
    ['foo' or 'bar', value]: value
    _: 10000
# => 100

match ['foo', 100]:
    [str x or int x, value]: value
    _: 10000
# => 100


# Record pattern
record Person(name, age)

foo = Person('foo', 32)

match foo:
    Person('bar', age):
        'bar:' + str(age)
    Person('foo', age):
        'foo:' + str(age)
    _: None
# => 'foo:32'

Records

record Mochi
record AnkoMochi(anko) < Mochi
record KinakoMochi(kinako) < Mochi

anko_mochi = AnkoMochi(anko=3)

isinstance(anko_mochi, Mochi)
# => True
isinstance(anko_mochi, AnkoMochi)
# => True
isinstance(anko_mochi, KinakoMochi)
# => False

match anko_mochi:
    KinakoMochi(kinako): 'kinako ' * kinako + ' mochi'
    AnkoMochi(anko): 'anko ' * anko + 'mochi'
    Mochi(_): 'mochi'
# => 'anko anko anko mochi'


record Person(name, age):
    def show(self):
        print(self.name + ': ' + self.age)

foo = Person('foo', '32')
foo.show()
# -> foo: 32

# runtime type checking
record Point(x:int, y:int, z:optional(int))
Point(1, 2, None)
# => Point(x=1, y=2, z=None)
Point(1, 2, 3)
# => Point(x=1, y=2, z=3)
Point(1, None, 3)
# => TypeError

Bindings

x = 3000
# => 3000

[a, b] = [1, 2]
a
# => 1
b
# => 2

[c, &d] = [1, 2, 3]
c
# => 1
d
# => pvector([2, 3])

Data types, like algebraic data types

data Point:
    Point2D(x, y)
    Point3D(x, y, z)

# The meaning of the above is the same as the meaning of the following.
# record Point
# record Point2D(x, y) < Point
# record Point3D(x, y, z) < Point

p1 = Point2D(x=1, y=2)
# => Point2D(x=1, y=2)

p2 = Point2D(3, 4)
# => Point2D(x=3, y=4)

p1.x
# => 1

Pattern-matching function definitions

data Point:
    Point2D(x, y)
    Point3D(x, y, z)

def offset:
    Point2D(x1, y1), Point2D(x2, y2):
        Point2D(x1 + x2, y1 + y2)
    Point3D(x1, y1, z1), Point3D(x2, y2, z2):
        Point3D(x1 + x2, y1 + y2, z1 + z2)
    _: None

offset(Point2D(1, 2), Point2D(3, 4))
# => Point2D(x=4, y=6)
offset(Point3D(1, 2, 3), Point3D(4, 5, 6))
# => Point3D(x=5, y=7, z=9)

def show:
    int x, message: print('int', x, message)
    float x, message: print('float', x, message)
    _: None

show(1.0, 'msg')
# -> float 1.0 msg
# => None

FileMode = options('r', 'w', 'a', 'r+', 'w+', 'a+')

def open_file:
    str path, FileMode mode: 
        open(path, mode)
    str path:
        open(path, 'r')

Anonymous function

# Arrow expression.
add = (x, y) -> x + y
add(1, 2)
# => 3

add = -> $1 + $2
add(1, 2)
# => 3

foo = (x, y) ->
    if x == 0:
        y
    else:
        x

foo(1, 2)
# => 1

foo(0, 2)
# => 2

pvector(map(-> $1 * 2, [1, 2, 3]))
# => pvector([2, 4, 6])

Pipeline operator

add = -> $1 + $2
2 |> add(10) |> add(12)
# => 24
None |>? add(10) |>? add(12)
# => None

Lazy sequences

def fizzbuzz(n):
    match [n % 3, n % 5]:
        [0, 0]: "fizzbuzz"
        [0, _]: "fizz"
        [_, 0]: "buzz"
        _: n


result = range(1, 31) |> map(fizzbuzz)
pvector(result)
# => pvector([1, 2, fizz, 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz', 16, 17, 'fizz', 19, 'buzz', 'fizz', 22, 23, 'fizz', 'buzz', 26, 'fizz', 28, 29, 'fizzbuzz'])
pvector(result)
# => pvector([])
pvector(result)
# => pvector([])


# Iterator -> lazyseq
lazy_result = range(1, 31) |> map(fizzbuzz) |> lazyseq()
pvector(lazy_result)
# => pvector([1, 2, fizz, 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz', 16, 17, 'fizz', 19, 'buzz', 'fizz', 22, 23, 'fizz', 'buzz', 26, 'fizz', 28, 29, 'fizzbuzz'])
pvector(lazy_result)
# => pvector([1, 2, fizz, 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz', 16, 17, 'fizz', 19, 'buzz', 'fizz', 22, 23, 'fizz', 'buzz', 26, 'fizz', 28, 29, 'fizzbuzz'])
pvector(lazy_result)
# => pvector([1, 2, fizz, 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz', 16, 17, 'fizz', 19, 'buzz', 'fizz', 22, 23, 'fizz', 'buzz', 26, 'fizz', 28, 29, 'fizzbuzz'])

Trailing closures

# The following trailing closure expression is passed to a function as the function’s first argument.
result = map([1, 2, 3]) ->
    print($1)
    $1 * 2

print(doall(result))

# -> 1
# -> 2
# -> 3
# => pvector([2, 4, 6])


def foreach(closure, seq):
    doall(filter(closure, seq))

# The following trailing closure expression is passed to a function as the function’s first argument.
foreach([1, 2, 3]) (item) ->
    new_item = item * 100
    print(new_item)

# -> 100
# -> 200
# -> 300
# => pvector([])

# Or

def foreach(seq, closure):
    doall(filter(closure, seq))

# The following trailing closure expression is passed to a function as the function’s final argument.
foreach([1, 2, 3]) @ (item) ->
    new_item = item * 100
    print(new_item)

# -> 100
# -> 200
# -> 300
# => pvector([])

Short form for keyword arguments and dict keys

def foo(a, b, c):
    a + b + c
    
a = 1
b = 2
c = 3

# This is the same as foo(a=a, b=b, c=c)
foo(=a, =b, =c))
# => 6

# This is the same as {'a': a, 'b': b}
{=a, =b}
# => pmap({'a': 1, 'b': 2})

Macros

macro rest_if_first_is_true(first, &args):
     match first:
         quote(True): quasi_quote(v(unquote_splicing(args)))
         _: quote(False)

rest_if_first_is_true(True, 1, 2, 3)
# => pvector([1, 2, 3])
rest_if_first_is_true("foo", 1, 2, 3)
# => False

macro pipeline(&args):
    [Symbol('|>')] + args

pipeline([1, 2, 3],
         map(-> $1 * 2),
         filter(-> $1 != 2),
         pvector())
# => pvector([4, 6])

Including a file at compile time

$ cat anko.mochi
x = 10000
y = 20000
require 'anko.mochi'
x
# => 10000

x = 30000

require 'anko.mochi' # include once at compile time
x
# => 30000

Module

module Math:
    export add, sub
    
    def add(x, y):
        x + y
    
    def sub(x, y):
        x - y

Math.add(1, 2)
# => 3
$ cat foobar.mochi
foo = 'foo'
bar = 'bar'
require 'foobar.mochi'
[foo, bar]
# => pvector(['foo', 'bar'])

foo = 'foofoofoo'

module X:
    export foobar
    require 'foobar.mochi'
    def foobar:
        [foo, bar]

X.foobar()
# => pvector(['foo', 'bar'])

[foo, bar]
# => pvector(['foofoofoo', 'bar'])

TODO

  • Improve documentation
  • Improve parsing
  • Support type annotation

License

MIT License

Contributors

https://github.com/i2y/mochi/graphs/contributors

About

Dynamically typed functional programming language

Resources

License

Stars

Watchers

Forks

Packages

No packages published