Skip to content

Commit

Permalink
added haskell code generator
Browse files Browse the repository at this point in the history
  • Loading branch information
StrikerRUS committed Apr 8, 2020
1 parent 90890d8 commit 88a1416
Show file tree
Hide file tree
Showing 16 changed files with 530 additions and 7 deletions.
10 changes: 5 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ dist: xenial
language: python
python:
- 3.5
- 3.6
- 3.7
# - 3.6
# - 3.7

env:
- TEST=API
- TEST=E2E LANG="c_lang or python or java or go_lang or javascript or php"
- TEST=E2E LANG="c_sharp or visual_basic or powershell"
- TEST=E2E LANG="r_lang or dart"
- TEST=E2E LANG="haskell"
# - TEST=E2E LANG="c_sharp or visual_basic or powershell"
# - TEST=E2E LANG="r_lang or dart"

before_install:
- bash .travis/setup.sh
Expand Down
6 changes: 6 additions & 0 deletions .travis/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@ if [[ $LANG == *"dart"* ]]; then
sudo apt-get update
sudo apt-get install --no-install-recommends -y dart
fi

# Install Haskell.
if [[ $LANG == *"haskell"* ]]; then
sudo apt-get update
sudo apt-get install --no-install-recommends -y haskell-platform
fi
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![PyPI Version](https://img.shields.io/pypi/v/m2cgen.svg?logo=pypi&logoColor=white)](https://pypi.org/project/m2cgen)
[![Downloads](https://pepy.tech/badge/m2cgen)](https://pepy.tech/project/m2cgen)

**m2cgen** (Model 2 Code Generator) - is a lightweight library which provides an easy way to transpile trained statistical models into a native code (Python, C, Java, Go, JavaScript, Visual Basic, C#, PowerShell, R, PHP, Dart).
**m2cgen** (Model 2 Code Generator) - is a lightweight library which provides an easy way to transpile trained statistical models into a native code (Python, C, Java, Go, JavaScript, Visual Basic, C#, PowerShell, R, PHP, Dart, Haskell).

* [Installation](#installation)
* [Supported Languages](#supported-languages)
Expand All @@ -30,6 +30,7 @@ pip install m2cgen
- C#
- Dart
- Go
- Haskell
- Java
- JavaScript
- PHP
Expand Down
2 changes: 2 additions & 0 deletions m2cgen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
export_to_r,
export_to_php,
export_to_dart,
export_to_haskell,
)

__all__ = [
Expand All @@ -26,6 +27,7 @@
export_to_r,
export_to_php,
export_to_dart,
export_to_haskell,
]

with open(os.path.join(os.path.dirname(os.path.realpath(__file__)),
Expand Down
1 change: 1 addition & 0 deletions m2cgen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"r": (m2cgen.export_to_r, ["indent", "function_name"]),
"php": (m2cgen.export_to_php, ["indent", "function_name"]),
"dart": (m2cgen.export_to_dart, ["indent", "function_name"]),
"haskell": (m2cgen.export_to_haskell, ["indent", "function_name"]),
}


Expand Down
28 changes: 28 additions & 0 deletions m2cgen/exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,34 @@ def export_to_dart(model, indent=4, function_name="score"):
return _export(model, interpreter)


def export_to_haskell(model, module_name="Model", indent=4,
function_name="score"):
"""
Generates a Haskell code representation of the given model.
Parameters
----------
model : object
The model object that should be transpiled into code.
module_name : string, optional
The name of the generated module.
indent : int, optional
The size of indents in the generated code.
function_name : string, optional
Name of the function in the generated code.
Returns
-------
code : string
"""
interpreter = interpreters.HaskellInterpreter(
module_name=module_name,
indent=indent,
function_name=function_name,
)
return _export(model, interpreter)


def _export(model, interpreter):
assembler_cls = assemblers.get_assembler_cls(model)
model_ast = assembler_cls(model).assemble()
Expand Down
2 changes: 2 additions & 0 deletions m2cgen/interpreters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .r.interpreter import RInterpreter
from .php.interpreter import PhpInterpreter
from .dart.interpreter import DartInterpreter
from .haskell.interpreter import HaskellInterpreter

__all__ = [
JavaInterpreter,
Expand All @@ -22,4 +23,5 @@
RInterpreter,
PhpInterpreter,
DartInterpreter,
HaskellInterpreter,
]
2 changes: 1 addition & 1 deletion m2cgen/interpreters/go/linear_algebra.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ func mulVectorNumber(v1 []float64, num float64) []float64 {
result[i] = v1[i] * num
}
return result
}
}
Empty file.
50 changes: 50 additions & 0 deletions m2cgen/interpreters/haskell/code_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import contextlib

from m2cgen.ast import CompOpType
from m2cgen.interpreters.code_generator import BaseCodeGenerator, CodeTemplate


class HaskellCodeGenerator(BaseCodeGenerator):
tpl_num_value = CodeTemplate("${value}")
tpl_infix_expression = CodeTemplate("(${left}) ${op} (${right})")
tpl_module_definition = CodeTemplate("module ${module_name} where")

def __init__(self, *args, **kwargs):
super(HaskellCodeGenerator, self).__init__(*args, **kwargs)

def array_index_access(self, array_name, index):
return self.tpl_infix_expression(
left=array_name, op="!!", right=index)

def function_invocation(self, function_name, *args):
return (function_name + " " +
" ".join(map(lambda x: "({})".format(x), args)))

def add_function_def(self, name, args, is_scalar_output):
signature = name + " :: "
signature += " -> ".join(
["[Double]" if is_vector else "Double"
for is_vector, _ in [*args, (not is_scalar_output, None)]])
self.add_code_line(signature)

function_def = name + " "
function_def += " ".join([n for _, n in args])
function_def += " ="
self.add_code_line(function_def)

self.increase_indent()

@contextlib.contextmanager
def function_definition(self, name, args, is_scalar_output):
self.add_function_def(name, args, is_scalar_output)
yield
self.decrease_indent()

def vector_init(self, values):
return "[" + ", ".join(values) + "]"

def _comp_op_overwrite(self, op):
if op == CompOpType.NOT_EQ:
return "/="
else:
return op.value
58 changes: 58 additions & 0 deletions m2cgen/interpreters/haskell/interpreter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import os

from m2cgen import ast
from m2cgen.interpreters import mixins, utils
from m2cgen.interpreters.haskell.code_generator import HaskellCodeGenerator
from m2cgen.interpreters.interpreter import ToCodeInterpreter


class HaskellInterpreter(ToCodeInterpreter,
mixins.LinearAlgebraMixin):
supported_bin_vector_ops = {
ast.BinNumOpType.ADD: "addVectors",
}

supported_bin_vector_num_ops = {
ast.BinNumOpType.MUL: "mulVectorNumber",
}

exponent_function_name = "exp"
tanh_function_name = "tanh"

def __init__(self, module_name="Model", indent=4, function_name="score",
*args, **kwargs):
self.module_name = module_name
self.function_name = function_name

cg = HaskellCodeGenerator(indent=indent)
super(HaskellInterpreter, self).__init__(cg, *args, **kwargs)

def interpret(self, expr):
self._cg.reset_state()
self._reset_reused_expr_cache()

self._cg.add_code_line(self._cg.tpl_module_definition(
module_name=self.module_name))

args = [(True, self._feature_array_name)]
func_name = self.function_name

with self._cg.function_definition(
name=func_name,
args=args,
is_scalar_output=expr.output_size == 1):
last_result = self._do_interpret(expr)
self._cg.add_code_line(last_result)

if self.with_linear_algebra:
filename = os.path.join(
os.path.dirname(__file__), "linear_algebra.hs")
self._cg.prepend_code_lines(utils.get_file_content(filename))

return self._cg.code

def interpret_pow_expr(self, expr, **kwargs):
base_result = self._do_interpret(expr.base_expr, **kwargs)
exp_result = self._do_interpret(expr.exp_expr, **kwargs)
return self._cg.infix_expression(
left=base_result, right=exp_result, op="**")
5 changes: 5 additions & 0 deletions m2cgen/interpreters/haskell/linear_algebra.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
addVectors :: [Double] -> [Double] -> [Double]
addVectors v1 v2 = zipWith (+) v1 v2

mulVectorNumber :: [Double] -> Double -> [Double]
mulVectorNumber v1 num = [i * num | i <- v1]
2 changes: 2 additions & 0 deletions tests/e2e/executors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from tests.e2e.executors.r import RExecutor
from tests.e2e.executors.php import PhpExecutor
from tests.e2e.executors.dart import DartExecutor
from tests.e2e.executors.haskell import HaskellExecutor

__all__ = [
JavaExecutor,
Expand All @@ -22,4 +23,5 @@
RExecutor,
PhpExecutor,
DartExecutor,
HaskellExecutor,
]
71 changes: 71 additions & 0 deletions tests/e2e/executors/haskell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import os
import string
import subprocess

from m2cgen import assemblers, interpreters
from tests import utils
from tests.e2e.executors import base

EXECUTOR_CODE_TPL = """
module Main where
import System.Environment (getArgs)
import ${model_name}
main = do
args <- getArgs
let inputArray = [read i::Double | i <- args]
let res = score inputArray
${print_code}
"""

EXECUTE_AND_PRINT_SCALAR = "print res"

EXECUTE_AND_PRINT_VECTOR = \
r"""mapM_ (putStr . \x -> show x ++ " ") res"""


class HaskellExecutor(base.BaseExecutor):

executor_name = "Main"
model_name = "Model"

def __init__(self, model):
self.model = model
self.interpreter = interpreters.HaskellInterpreter()

assembler_cls = assemblers.get_assembler_cls(model)
self.model_ast = assembler_cls(model).assemble()

self._ghc = "ghc"

def predict(self, X):
app_name = os.path.join(self._resource_tmp_dir,
self.executor_name)
exec_args = [app_name, *map(str, X)]
return utils.predict_from_commandline(exec_args)

def prepare(self):
if self.model_ast.output_size > 1:
print_code = EXECUTE_AND_PRINT_VECTOR
else:
print_code = EXECUTE_AND_PRINT_SCALAR
executor_code = string.Template(EXECUTOR_CODE_TPL).substitute(
model_name=self.model_name,
print_code=print_code)
model_code = self.interpreter.interpret(self.model_ast)

executor_file_name = os.path.join(
self._resource_tmp_dir, "{}.hs".format(self.executor_name))
model_file_name = os.path.join(
self._resource_tmp_dir, "{}.hs".format(self.model_name))
with open(executor_file_name, "w") as f:
f.write(executor_code)
with open(model_file_name, "w") as f:
f.write(model_code)

exec_args = [self._ghc, executor_file_name,
"-i{}".format(self._resource_tmp_dir),
"-o", os.path.join(self._resource_tmp_dir,
self.executor_name)]
subprocess.call(exec_args)
2 changes: 2 additions & 0 deletions tests/e2e/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
R = pytest.mark.r_lang
PHP = pytest.mark.php
DART = pytest.mark.dart
HASKELL = pytest.mark.haskell
REGRESSION = pytest.mark.regr
CLASSIFICATION = pytest.mark.clf

Expand Down Expand Up @@ -129,6 +130,7 @@ def classification_binary_random(model):
(executors.RExecutor, R),
(executors.PhpExecutor, PHP),
(executors.DartExecutor, DART),
(executors.HaskellExecutor, HASKELL),
],
# These models will be tested against each language specified in the
Expand Down
Loading

0 comments on commit 88a1416

Please sign in to comment.