Repository: BayesWitnesses/m2cgen (language: rust)

Issue:
add support for declarative languages
It seems that especially for functional languages it is very common to write `Score()` function and apply it to some data.

At present, assemblers make assumption about that target language is imperative.


Discussion / hints:
Hey @StrikerRUS. Not sure I understand what you mean. Assemblers make no assumptions of this sort. The entire AST that comes out of assembler is a single expression which can be interpreted in a purely functional fashion. Except for perhaps Subroutines which can be safely ignored if needed.
Perhaps you can give a more specific example?
Hmmm, then seems that the base classes of code generator and interpreter confused me when suggested to override variable definition and if statement.
> then seems that the base classes of code generator and interpreter confused me when suggested to override variable definition and if statement.

That's true. So far we only added support for imperative languages and we haven't had need/demand to add functional ones. This is why the current interpreter/codegen class hierarchy only reflects imperative style. The AST and assemblers, however, have no coupling of this sort.
Thank you very much for the detailed explanation!
Feel free to close the issue. Or you can leave it as a feature request 🙂 .

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index 7b70f7f5..6b30c23c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,7 +9,7 @@ python:
 
 env:
   - TEST=API
-  - TEST=E2E LANG="c_lang or python or java or go_lang or javascript or php"
+  - TEST=E2E LANG="c_lang or python or java or go_lang or javascript or php or haskell"
   - TEST=E2E LANG="c_sharp or visual_basic or powershell"
   - TEST=E2E LANG="r_lang or dart"
 
diff --git a/.travis/setup.sh b/.travis/setup.sh
index b2db0808..41be7375 100644
--- a/.travis/setup.sh
+++ b/.travis/setup.sh
@@ -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
diff --git a/Dockerfile b/Dockerfile
index 7ff63c18..185f56ae 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -26,7 +26,8 @@ RUN apt-get update && \
         powershell \
         r-base \
         php \
-        dart && \
+        dart \
+        haskell-platform && \
     rm -rf /var/lib/apt/lists/*
 
 WORKDIR /m2cgen
diff --git a/README.md b/README.md
index 54c8bb84..cec4b02f 100644
--- a/README.md
+++ b/README.md
@@ -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)
@@ -30,6 +30,7 @@ pip install m2cgen
 - C#
 - Dart
 - Go
+- Haskell
 - Java
 - JavaScript
 - PHP
diff --git a/m2cgen/__init__.py b/m2cgen/__init__.py
index 316d2150..d4834b39 100644
--- a/m2cgen/__init__.py
+++ b/m2cgen/__init__.py
@@ -12,6 +12,7 @@
     export_to_r,
     export_to_php,
     export_to_dart,
+    export_to_haskell,
 )
 
 __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__)),
diff --git a/m2cgen/cli.py b/m2cgen/cli.py
index 84cb6ef9..35a2bcae 100644
--- a/m2cgen/cli.py
+++ b/m2cgen/cli.py
@@ -32,6 +32,8 @@
     "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,
+                ["module_name", "indent", "function_name"]),
 }
 
 
diff --git a/m2cgen/exporters.py b/m2cgen/exporters.py
index ca9b9cf3..ff145e11 100644
--- a/m2cgen/exporters.py
+++ b/m2cgen/exporters.py
@@ -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()
diff --git a/m2cgen/interpreters/__init__.py b/m2cgen/interpreters/__init__.py
index 87042690..002cf55e 100644
--- a/m2cgen/interpreters/__init__.py
+++ b/m2cgen/interpreters/__init__.py
@@ -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,
@@ -22,4 +23,5 @@
     RInterpreter,
     PhpInterpreter,
     DartInterpreter,
+    HaskellInterpreter,
 ]
diff --git a/m2cgen/interpreters/c/interpreter.py b/m2cgen/interpreters/c/interpreter.py
index 083a0b7a..8c5a2ee6 100644
--- a/m2cgen/interpreters/c/interpreter.py
+++ b/m2cgen/interpreters/c/interpreter.py
@@ -3,10 +3,10 @@
 from m2cgen import ast
 from m2cgen.interpreters import utils, mixins
 from m2cgen.interpreters.c.code_generator import CCodeGenerator
-from m2cgen.interpreters.interpreter import ToCodeInterpreter
+from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter
 
 
-class CInterpreter(ToCodeInterpreter,
+class CInterpreter(ImperativeToCodeInterpreter,
                    mixins.LinearAlgebraMixin):
 
     supported_bin_vector_ops = {
diff --git a/m2cgen/interpreters/c_sharp/interpreter.py b/m2cgen/interpreters/c_sharp/interpreter.py
index 6f691362..6e32b763 100644
--- a/m2cgen/interpreters/c_sharp/interpreter.py
+++ b/m2cgen/interpreters/c_sharp/interpreter.py
@@ -3,11 +3,12 @@
 from m2cgen import ast
 from m2cgen.interpreters import mixins
 from m2cgen.interpreters import utils
-from m2cgen.interpreters.interpreter import ToCodeInterpreter
+from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter
 from m2cgen.interpreters.c_sharp.code_generator import CSharpCodeGenerator
 
 
-class CSharpInterpreter(ToCodeInterpreter, mixins.LinearAlgebraMixin):
+class CSharpInterpreter(ImperativeToCodeInterpreter,
+                        mixins.LinearAlgebraMixin):
 
     supported_bin_vector_ops = {
         ast.BinNumOpType.ADD: "AddVectors",
diff --git a/m2cgen/interpreters/code_generator.py b/m2cgen/interpreters/code_generator.py
index b2e32066..bf8d2f82 100644
--- a/m2cgen/interpreters/code_generator.py
+++ b/m2cgen/interpreters/code_generator.py
@@ -25,13 +25,7 @@ class BaseCodeGenerator:
 
     tpl_num_value = NotImplemented
     tpl_infix_expression = NotImplemented
-    tpl_var_declaration = NotImplemented
-    tpl_return_statement = NotImplemented
-    tpl_if_statement = NotImplemented
-    tpl_else_statement = NotImplemented
     tpl_array_index_access = NotImplemented
-    tpl_block_termination = NotImplemented
-    tpl_var_assignment = NotImplemented
 
     def __init__(self, indent=4):
         self._indent = indent
@@ -39,14 +33,8 @@ def __init__(self, indent=4):
 
     def reset_state(self):
         self._current_indent = 0
-        self._var_idx = 0
         self.code = ""
 
-    def get_var_name(self):
-        var_name = "var" + str(self._var_idx)
-        self._var_idx += 1
-        return var_name
-
     def increase_indent(self):
         self._current_indent += self._indent
 
@@ -78,6 +66,53 @@ def prepend_code_lines(self, lines):
         for l in lines[::-1]:
             self.prepend_code_line(l)
 
+    # Following methods simply compute expressions using templates without
+    # changing result.
+
+    def infix_expression(self, left, right, op):
+        return self.tpl_infix_expression(left=left, right=right, op=op)
+
+    def num_value(self, value):
+        return self.tpl_num_value(value=value)
+
+    def array_index_access(self, array_name, index):
+        return self.tpl_array_index_access(
+            array_name=array_name, index=index)
+
+    def function_invocation(self, function_name, *args):
+        return function_name + "(" + ", ".join(map(str, args)) + ")"
+
+    # Helpers
+
+    def _comp_op_overwrite(self, op):
+        return op.value
+
+
+class ImperativeCodeGenerator(BaseCodeGenerator):
+    """
+    This class provides basic functionality to generate code. It is
+    language-agnostic, but exposes set of attributes which subclasses should
+    use to define syntax specific for certain language(s).
+
+    !!IMPORTANT!!: Code generators must know nothing about AST.
+    """
+
+    tpl_var_declaration = NotImplemented
+    tpl_return_statement = NotImplemented
+    tpl_if_statement = NotImplemented
+    tpl_else_statement = NotImplemented
+    tpl_block_termination = NotImplemented
+    tpl_var_assignment = NotImplemented
+
+    def reset_state(self):
+        super().reset_state()
+        self._var_idx = 0
+
+    def get_var_name(self):
+        var_name = "var" + str(self._var_idx)
+        self._var_idx += 1
+        return var_name
+
     # Following statements compute expressions using templates AND add
     # it to the result.
 
@@ -110,32 +145,13 @@ def add_var_assignment(self, var_name, value, value_size):
         self.add_code_line(
             self.tpl_var_assignment(var_name=var_name, value=value))
 
-    # Following methods simply compute expressions using templates without
-    # changing result.
-
-    def infix_expression(self, left, right, op):
-        return self.tpl_infix_expression(left=left, right=right, op=op)
-
-    def num_value(self, value):
-        return self.tpl_num_value(value=value)
-
-    def array_index_access(self, array_name, index):
-        return self.tpl_array_index_access(
-            array_name=array_name, index=index)
-
-    def function_invocation(self, function_name, *args):
-        return function_name + "(" + ", ".join(map(str, args)) + ")"
-
     # Helpers
 
     def _get_var_declare_type(self, expr):
         return NotImplemented
 
-    def _comp_op_overwrite(self, op):
-        return op.value
-
 
-class CLikeCodeGenerator(BaseCodeGenerator):
+class CLikeCodeGenerator(ImperativeCodeGenerator):
     """
     This code generator provides C-like syntax so that subclasses will only
     have to provide logic for wrapping expressions into functions/classes/etc.
diff --git a/m2cgen/interpreters/dart/interpreter.py b/m2cgen/interpreters/dart/interpreter.py
index d085cfd5..34d658e3 100644
--- a/m2cgen/interpreters/dart/interpreter.py
+++ b/m2cgen/interpreters/dart/interpreter.py
@@ -3,11 +3,11 @@
 from m2cgen import ast
 from m2cgen.interpreters import mixins
 from m2cgen.interpreters import utils
-from m2cgen.interpreters.interpreter import ToCodeInterpreter
+from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter
 from m2cgen.interpreters.dart.code_generator import DartCodeGenerator
 
 
-class DartInterpreter(ToCodeInterpreter,
+class DartInterpreter(ImperativeToCodeInterpreter,
                       mixins.LinearAlgebraMixin,
                       mixins.BinExpressionDepthTrackingMixin):
 
diff --git a/m2cgen/interpreters/go/code_generator.py b/m2cgen/interpreters/go/code_generator.py
index 20dd1f6a..9f1782b0 100644
--- a/m2cgen/interpreters/go/code_generator.py
+++ b/m2cgen/interpreters/go/code_generator.py
@@ -1,9 +1,10 @@
 import contextlib
 
-from m2cgen.interpreters.code_generator import BaseCodeGenerator, CodeTemplate
+from m2cgen.interpreters.code_generator \
+    import ImperativeCodeGenerator, CodeTemplate
 
 
-class GoCodeGenerator(BaseCodeGenerator):
+class GoCodeGenerator(ImperativeCodeGenerator):
     tpl_num_value = CodeTemplate("${value}")
     tpl_infix_expression = CodeTemplate("(${left}) ${op} (${right})")
     tpl_array_index_access = CodeTemplate("${array_name}[${index}]")
diff --git a/m2cgen/interpreters/go/interpreter.py b/m2cgen/interpreters/go/interpreter.py
index 0f7e4384..95ca0ee2 100644
--- a/m2cgen/interpreters/go/interpreter.py
+++ b/m2cgen/interpreters/go/interpreter.py
@@ -3,10 +3,10 @@
 from m2cgen import ast
 from m2cgen.interpreters import mixins, utils
 from m2cgen.interpreters.go.code_generator import GoCodeGenerator
-from m2cgen.interpreters.interpreter import ToCodeInterpreter
+from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter
 
 
-class GoInterpreter(ToCodeInterpreter,
+class GoInterpreter(ImperativeToCodeInterpreter,
                     mixins.LinearAlgebraMixin):
     supported_bin_vector_ops = {
         ast.BinNumOpType.ADD: "addVectors",
diff --git a/m2cgen/interpreters/go/linear_algebra.go b/m2cgen/interpreters/go/linear_algebra.go
index 85be7dd7..ab797044 100644
--- a/m2cgen/interpreters/go/linear_algebra.go
+++ b/m2cgen/interpreters/go/linear_algebra.go
@@ -11,4 +11,4 @@ func mulVectorNumber(v1 []float64, num float64) []float64 {
         result[i] = v1[i] * num
     }
     return result
-}
\ No newline at end of file
+}
diff --git a/m2cgen/interpreters/haskell/__init__.py b/m2cgen/interpreters/haskell/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/m2cgen/interpreters/haskell/code_generator.py b/m2cgen/interpreters/haskell/code_generator.py
new file mode 100644
index 00000000..5b7a9312
--- /dev/null
+++ b/m2cgen/interpreters/haskell/code_generator.py
@@ -0,0 +1,81 @@
+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):
+        self._func_idx = 0
+        super().__init__(*args, **kwargs)
+
+    def reset_state(self):
+        super().reset_state()
+        self._func_idx = 0
+
+    def array_index_access(self, array_name, index):
+        return self.tpl_infix_expression(
+            left=array_name, op="!!", right=index)
+
+    def add_if_statement(self, if_def):
+        self.add_code_line("if ({})".format(if_def))
+        self.increase_indent()
+        self.add_code_line("then")
+        self.increase_indent()
+
+    def add_else_statement(self):
+        self.decrease_indent()
+        self.add_code_line("else")
+        self.increase_indent()
+
+    def add_if_termination(self):
+        self.decrease_indent()
+        self.decrease_indent()
+
+    def get_func_name(self):
+        func_name = "func" + str(self._func_idx)
+        self._func_idx += 1
+        return func_name
+
+    def add_function(self, function_name, function_body):
+        self.add_code_line("{} =".format(function_name))
+        self.increase_indent()
+        self.add_code_lines(function_body)
+        self.decrease_indent()
+
+    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
diff --git a/m2cgen/interpreters/haskell/interpreter.py b/m2cgen/interpreters/haskell/interpreter.py
new file mode 100644
index 00000000..577aa8af
--- /dev/null
+++ b/m2cgen/interpreters/haskell/interpreter.py
@@ -0,0 +1,101 @@
+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"
+    sqrt_function_name = "sqrt"
+    tanh_function_name = "tanh"
+
+    def __init__(self,  module_name="Model", indent=4, function_name="score",
+                 *args, **kwargs):
+        self.module_name = module_name
+        self.indent = indent
+        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()
+
+        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)
+            self._dump_cache()
+
+        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))
+
+        self._cg.prepend_code_line(self._cg.tpl_module_definition(
+            module_name=self.module_name))
+
+        return self._cg.code
+
+    def interpret_if_expr(self, expr, if_code_gen=None, **kwargs):
+        if if_code_gen is None:
+            code_gen = HaskellCodeGenerator(indent=self.indent)
+            nested = False
+        else:
+            code_gen = if_code_gen
+            nested = True
+
+        code_gen.add_if_statement(self._do_interpret(
+            expr.test, **kwargs))
+        code_gen.add_code_line(self._do_interpret(
+            expr.body, if_code_gen=code_gen, **kwargs))
+        code_gen.add_else_statement()
+        code_gen.add_code_line(self._do_interpret(
+            expr.orelse, if_code_gen=code_gen, **kwargs))
+        code_gen.add_if_termination()
+
+        if not nested:
+            return self._cache_reused_expr(expr, code_gen.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="**")
+
+    # Cached expressions become functions with no arguments, i.e. values
+    # which are CAFs. Therefore, they are computed only once.
+    def _cache_reused_expr(self, expr, expr_result):
+        if expr in self._cached_expr_results:
+            return self._cached_expr_results[expr].var_name
+        else:
+            func_name = self._cg.get_func_name()
+            self._cached_expr_results[expr] = utils.CachedResult(
+                var_name=func_name, expr_result=expr_result)
+            return func_name
+
+    def _dump_cache(self):
+        if self._cached_expr_results:
+            self._cg.add_code_line("where")
+            self._cg.increase_indent()
+            for func_name, expr_result in self._cached_expr_results.values():
+                self._cg.add_function(
+                    function_name=func_name, function_body=expr_result)
+            self._cg.decrease_indent()
diff --git a/m2cgen/interpreters/haskell/linear_algebra.hs b/m2cgen/interpreters/haskell/linear_algebra.hs
new file mode 100644
index 00000000..33adfd0d
--- /dev/null
+++ b/m2cgen/interpreters/haskell/linear_algebra.hs
@@ -0,0 +1,4 @@
+addVectors :: [Double] -> [Double] -> [Double]
+addVectors v1 v2 = zipWith (+) v1 v2
+mulVectorNumber :: [Double] -> Double -> [Double]
+mulVectorNumber v1 num = [i * num | i <- v1]
diff --git a/m2cgen/interpreters/interpreter.py b/m2cgen/interpreters/interpreter.py
index f83083f6..a8d427af 100644
--- a/m2cgen/interpreters/interpreter.py
+++ b/m2cgen/interpreters/interpreter.py
@@ -1,6 +1,7 @@
 import re
 
 from m2cgen import ast
+from m2cgen.interpreters.utils import CachedResult
 
 
 class BaseInterpreter:
@@ -41,7 +42,7 @@ def _do_interpret(self, expr, to_reuse=None, **kwargs):
             return handler(expr, **kwargs)
 
         if expr in self._cached_expr_results:
-            return self._cached_expr_results[expr]
+            return self._cached_expr_results[expr].var_name
 
         result = handler(expr, **kwargs)
         return self._cache_reused_expr(expr, result)
@@ -100,28 +101,6 @@ def __init__(self, cg, feature_array_name="input"):
         self.with_vectors = False
         self.with_math_module = False
 
-    def interpret_if_expr(self, expr, if_var_name=None, **kwargs):
-        if if_var_name is not None:
-            var_name = if_var_name
-        else:
-            var_name = self._cg.add_var_declaration(expr.output_size)
-
-        def handle_nested_expr(nested):
-            if isinstance(nested, ast.IfExpr):
-                self._do_interpret(nested, if_var_name=var_name, **kwargs)
-            else:
-                nested_result = self._do_interpret(nested, **kwargs)
-                self._cg.add_var_assignment(var_name, nested_result,
-                                            nested.output_size)
-
-        self._cg.add_if_statement(self._do_interpret(expr.test, **kwargs))
-        handle_nested_expr(expr.body)
-        self._cg.add_else_statement()
-        handle_nested_expr(expr.orelse)
-        self._cg.add_block_termination()
-
-        return var_name
-
     def interpret_comp_expr(self, expr, **kwargs):
         op = self._cg._comp_op_overwrite(expr.op)
         return self._cg.infix_expression(
@@ -177,8 +156,44 @@ def interpret_pow_expr(self, expr, **kwargs):
         return self._cg.function_invocation(
             self.power_function_name, base_result, exp_result)
 
+
+class ImperativeToCodeInterpreter(ToCodeInterpreter):
+    """
+    This interpreter provides default implementation for the methods
+    interpreting AST expression into code.
+
+    It can be used for the most programming languages and requires only
+    language-specific instance of the CodeGenerator.
+
+    !!IMPORTANT!!: Code generators used by this interpreter must know nothing
+    about AST.
+    """
+
+    def interpret_if_expr(self, expr, if_var_name=None, **kwargs):
+        if if_var_name is not None:
+            var_name = if_var_name
+        else:
+            var_name = self._cg.add_var_declaration(expr.output_size)
+
+        def handle_nested_expr(nested):
+            if isinstance(nested, ast.IfExpr):
+                self._do_interpret(nested, if_var_name=var_name, **kwargs)
+            else:
+                nested_result = self._do_interpret(nested, **kwargs)
+                self._cg.add_var_assignment(var_name, nested_result,
+                                            nested.output_size)
+
+        self._cg.add_if_statement(self._do_interpret(expr.test, **kwargs))
+        handle_nested_expr(expr.body)
+        self._cg.add_else_statement()
+        handle_nested_expr(expr.orelse)
+        self._cg.add_block_termination()
+
+        return var_name
+
     def _cache_reused_expr(self, expr, expr_result):
         var_name = self._cg.add_var_declaration(expr.output_size)
         self._cg.add_var_assignment(var_name, expr_result, expr.output_size)
-        self._cached_expr_results[expr] = var_name
+        self._cached_expr_results[expr] = CachedResult(
+            var_name=var_name, expr_result=None)
         return var_name
diff --git a/m2cgen/interpreters/java/interpreter.py b/m2cgen/interpreters/java/interpreter.py
index 0adcb768..01b4d41a 100644
--- a/m2cgen/interpreters/java/interpreter.py
+++ b/m2cgen/interpreters/java/interpreter.py
@@ -3,11 +3,11 @@
 from m2cgen import ast
 from m2cgen.interpreters import mixins
 from m2cgen.interpreters import utils
-from m2cgen.interpreters.interpreter import ToCodeInterpreter
+from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter
 from m2cgen.interpreters.java.code_generator import JavaCodeGenerator
 
 
-class JavaInterpreter(ToCodeInterpreter,
+class JavaInterpreter(ImperativeToCodeInterpreter,
                       mixins.LinearAlgebraMixin,
                       mixins.SubroutinesMixin):
 
diff --git a/m2cgen/interpreters/javascript/interpreter.py b/m2cgen/interpreters/javascript/interpreter.py
index 34a346a5..09fb05e7 100644
--- a/m2cgen/interpreters/javascript/interpreter.py
+++ b/m2cgen/interpreters/javascript/interpreter.py
@@ -3,12 +3,12 @@
 from m2cgen import ast
 from m2cgen.interpreters import mixins
 from m2cgen.interpreters import utils
-from m2cgen.interpreters.interpreter import ToCodeInterpreter
+from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter
 from m2cgen.interpreters.javascript.code_generator \
     import JavascriptCodeGenerator
 
 
-class JavascriptInterpreter(ToCodeInterpreter,
+class JavascriptInterpreter(ImperativeToCodeInterpreter,
                             mixins.LinearAlgebraMixin):
 
     supported_bin_vector_ops = {
diff --git a/m2cgen/interpreters/php/interpreter.py b/m2cgen/interpreters/php/interpreter.py
index a0b44c3d..06d0416b 100644
--- a/m2cgen/interpreters/php/interpreter.py
+++ b/m2cgen/interpreters/php/interpreter.py
@@ -3,10 +3,11 @@
 from m2cgen import ast
 from m2cgen.interpreters import utils, mixins
 from m2cgen.interpreters.php.code_generator import PhpCodeGenerator
-from m2cgen.interpreters.interpreter import ToCodeInterpreter
+from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter
 
 
-class PhpInterpreter(ToCodeInterpreter, mixins.LinearAlgebraMixin):
+class PhpInterpreter(ImperativeToCodeInterpreter,
+                     mixins.LinearAlgebraMixin):
 
     supported_bin_vector_ops = {
         ast.BinNumOpType.ADD: "addVectors",
diff --git a/m2cgen/interpreters/powershell/interpreter.py b/m2cgen/interpreters/powershell/interpreter.py
index b628c241..3b75ab8c 100644
--- a/m2cgen/interpreters/powershell/interpreter.py
+++ b/m2cgen/interpreters/powershell/interpreter.py
@@ -4,10 +4,10 @@
 from m2cgen.interpreters import utils, mixins
 from m2cgen.interpreters.powershell.code_generator \
     import PowershellCodeGenerator
-from m2cgen.interpreters.interpreter import ToCodeInterpreter
+from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter
 
 
-class PowershellInterpreter(ToCodeInterpreter,
+class PowershellInterpreter(ImperativeToCodeInterpreter,
                             mixins.LinearAlgebraMixin):
 
     supported_bin_vector_ops = {
diff --git a/m2cgen/interpreters/python/code_generator.py b/m2cgen/interpreters/python/code_generator.py
index 2527905a..9c27ddd3 100644
--- a/m2cgen/interpreters/python/code_generator.py
+++ b/m2cgen/interpreters/python/code_generator.py
@@ -1,10 +1,10 @@
 import contextlib
 
-from m2cgen.interpreters.code_generator import BaseCodeGenerator
+from m2cgen.interpreters.code_generator import ImperativeCodeGenerator
 from m2cgen.interpreters.code_generator import CodeTemplate as CT
 
 
-class PythonCodeGenerator(BaseCodeGenerator):
+class PythonCodeGenerator(ImperativeCodeGenerator):
 
     tpl_num_value = CT("${value}")
     tpl_infix_expression = CT("(${left}) ${op} (${right})")
diff --git a/m2cgen/interpreters/python/interpreter.py b/m2cgen/interpreters/python/interpreter.py
index 50289272..e884ad75 100644
--- a/m2cgen/interpreters/python/interpreter.py
+++ b/m2cgen/interpreters/python/interpreter.py
@@ -1,9 +1,9 @@
 from m2cgen.interpreters import mixins
-from m2cgen.interpreters.interpreter import ToCodeInterpreter
+from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter
 from m2cgen.interpreters.python.code_generator import PythonCodeGenerator
 
 
-class PythonInterpreter(ToCodeInterpreter,
+class PythonInterpreter(ImperativeToCodeInterpreter,
                         mixins.BinExpressionDepthTrackingMixin,
                         mixins.LinearAlgebraMixin):
 
diff --git a/m2cgen/interpreters/r/interpreter.py b/m2cgen/interpreters/r/interpreter.py
index 5f6a4543..67d959d1 100644
--- a/m2cgen/interpreters/r/interpreter.py
+++ b/m2cgen/interpreters/r/interpreter.py
@@ -1,9 +1,9 @@
 from m2cgen.interpreters import mixins
-from m2cgen.interpreters.interpreter import ToCodeInterpreter
+from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter
 from m2cgen.interpreters.r.code_generator import RCodeGenerator
 
 
-class RInterpreter(ToCodeInterpreter,
+class RInterpreter(ImperativeToCodeInterpreter,
                    mixins.LinearAlgebraMixin,
                    mixins.BinExpressionDepthTrackingMixin,
                    mixins.SubroutinesMixin):
diff --git a/m2cgen/interpreters/utils.py b/m2cgen/interpreters/utils.py
index f5c60f9d..0292cb8b 100644
--- a/m2cgen/interpreters/utils.py
+++ b/m2cgen/interpreters/utils.py
@@ -1,3 +1,9 @@
+from collections import namedtuple
+
+
+CachedResult = namedtuple('CachedResult', ['var_name', 'expr_result'])
+
+
 def get_file_content(path):
     with open(path) as f:
         return f.read()
diff --git a/m2cgen/interpreters/visual_basic/code_generator.py b/m2cgen/interpreters/visual_basic/code_generator.py
index f59f4389..539c4b9e 100644
--- a/m2cgen/interpreters/visual_basic/code_generator.py
+++ b/m2cgen/interpreters/visual_basic/code_generator.py
@@ -1,10 +1,11 @@
 import contextlib
 
 from m2cgen.ast import CompOpType
-from m2cgen.interpreters.code_generator import BaseCodeGenerator, CodeTemplate
+from m2cgen.interpreters.code_generator \
+    import ImperativeCodeGenerator, CodeTemplate
 
 
-class VisualBasicCodeGenerator(BaseCodeGenerator):
+class VisualBasicCodeGenerator(ImperativeCodeGenerator):
     tpl_num_value = CodeTemplate("${value}")
     tpl_infix_expression = CodeTemplate("(${left}) ${op} (${right})")
     tpl_var_declaration = \
diff --git a/m2cgen/interpreters/visual_basic/interpreter.py b/m2cgen/interpreters/visual_basic/interpreter.py
index 63a3bcc2..01ab2c6e 100644
--- a/m2cgen/interpreters/visual_basic/interpreter.py
+++ b/m2cgen/interpreters/visual_basic/interpreter.py
@@ -2,12 +2,13 @@
 
 from m2cgen import ast
 from m2cgen.interpreters import mixins, utils
-from m2cgen.interpreters.interpreter import ToCodeInterpreter
+from m2cgen.interpreters.interpreter import ImperativeToCodeInterpreter
 from m2cgen.interpreters.visual_basic.code_generator \
     import VisualBasicCodeGenerator
 
 
-class VisualBasicInterpreter(ToCodeInterpreter, mixins.LinearAlgebraMixin):
+class VisualBasicInterpreter(ImperativeToCodeInterpreter,
+                             mixins.LinearAlgebraMixin):
     supported_bin_vector_ops = {
         ast.BinNumOpType.ADD: "AddVectors",
     }


Test patch:
diff --git a/tests/e2e/executors/__init__.py b/tests/e2e/executors/__init__.py
index 9547adb6..83b32b7f 100644
--- a/tests/e2e/executors/__init__.py
+++ b/tests/e2e/executors/__init__.py
@@ -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,
@@ -22,4 +23,5 @@
     RExecutor,
     PhpExecutor,
     DartExecutor,
+    HaskellExecutor,
 ]
diff --git a/tests/e2e/executors/haskell.py b/tests/e2e/executors/haskell.py
new file mode 100644
index 00000000..58e8dec4
--- /dev/null
+++ b/tests/e2e/executors/haskell.py
@@ -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 ${executor_name} 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(
+            executor_name=self.executor_name,
+            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)
diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py
index 8b6e8baa..f370a01c 100644
--- a/tests/e2e/test_e2e.py
+++ b/tests/e2e/test_e2e.py
@@ -30,6 +30,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
 
@@ -133,6 +134,7 @@ def classification_binary_random(model, test_fraction=0.02):
         (executors.RExecutor, R),
         (executors.PhpExecutor, PHP),
         (executors.DartExecutor, DART),
+        (executors.HaskellExecutor, HASKELL),
     ],
 
     # These models will be tested against each language specified in the
diff --git a/tests/interpreters/test_haskell.py b/tests/interpreters/test_haskell.py
new file mode 100644
index 00000000..ff513426
--- /dev/null
+++ b/tests/interpreters/test_haskell.py
@@ -0,0 +1,335 @@
+from m2cgen import ast
+from m2cgen.interpreters import HaskellInterpreter
+from tests import utils
+
+
+def test_if_expr():
+    expr = ast.IfExpr(
+        ast.CompExpr(ast.NumVal(1), ast.FeatureRef(0), ast.CompOpType.EQ),
+        ast.NumVal(2),
+        ast.NumVal(3))
+
+    expected_code = """
+module Model where
+score :: [Double] -> Double
+score input =
+    func0
+    where
+        func0 =
+            if ((1) == ((input) !! (0)))
+                then
+                    2
+                else
+                    3
+"""
+
+    interpreter = HaskellInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_bin_num_expr():
+    expr = ast.BinNumExpr(
+        ast.BinNumExpr(
+            ast.FeatureRef(0), ast.NumVal(-2), ast.BinNumOpType.DIV),
+        ast.NumVal(2),
+        ast.BinNumOpType.MUL)
+
+    expected_code = """
+module Model where
+score :: [Double] -> Double
+score input =
+    (((input) !! (0)) / (-2)) * (2)
+"""
+
+    interpreter = HaskellInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_dependable_condition():
+    left = ast.BinNumExpr(
+        ast.IfExpr(
+            ast.CompExpr(ast.NumVal(1),
+                         ast.NumVal(1),
+                         ast.CompOpType.EQ),
+            ast.NumVal(1),
+            ast.NumVal(2)),
+        ast.NumVal(2),
+        ast.BinNumOpType.ADD)
+
+    right = ast.BinNumExpr(ast.NumVal(1), ast.NumVal(2), ast.BinNumOpType.DIV)
+    bool_test = ast.CompExpr(left, right, ast.CompOpType.GTE)
+
+    expr = ast.IfExpr(bool_test, ast.NumVal(1), ast.FeatureRef(0))
+
+    expected_code_1 = """
+module Model where
+score :: [Double] -> Double
+score input =
+    func1
+    where
+        func0 =
+            if ((1) == (1))
+                then
+                    1
+                else
+                    2
+        func1 =
+            if (((func0) + (2)) >= ((1) / (2)))
+                then
+                    1
+                else
+                    (input) !! (0)
+"""
+    expected_code_2 = """
+module Model where
+score :: [Double] -> Double
+score input =
+    func1
+    where
+        func1 =
+            if (((func0) + (2)) >= ((1) / (2)))
+                then
+                    1
+                else
+                    (input) !! (0)
+        func0 =
+            if ((1) == (1))
+                then
+                    1
+                else
+                    2
+"""
+
+    interpreter = HaskellInterpreter()
+    actual_code = interpreter.interpret(expr)
+    # dicts in Python 3.5 have nondeterministic order of keys
+    utils.assert_code_equal_any_of(
+        actual_code, expected_code_1, expected_code_2)
+
+
+def test_nested_condition():
+    left = ast.BinNumExpr(
+        ast.IfExpr(
+            ast.CompExpr(ast.NumVal(1),
+                         ast.NumVal(1),
+                         ast.CompOpType.EQ),
+            ast.NumVal(1),
+            ast.NumVal(2)),
+        ast.NumVal(2),
+        ast.BinNumOpType.ADD)
+
+    bool_test = ast.CompExpr(ast.NumVal(1), left, ast.CompOpType.EQ)
+
+    expr_nested = ast.IfExpr(bool_test, ast.FeatureRef(2), ast.NumVal(2))
+
+    expr = ast.IfExpr(bool_test, expr_nested, ast.NumVal(2))
+
+    expected_code_1 = """
+module Model where
+score :: [Double] -> Double
+score input =
+    func1
+    where
+        func0 =
+            if ((1) == (1))
+                then
+                    1
+                else
+                    2
+        func1 =
+            if ((1) == ((func0) + (2)))
+                then
+                    if ((1) == ((func0) + (2)))
+                        then
+                            (input) !! (2)
+                        else
+                            2
+                else
+                    2
+"""
+    expected_code_2 = """
+module Model where
+score :: [Double] -> Double
+score input =
+    func1
+    where
+        func1 =
+            if ((1) == ((func0) + (2)))
+                then
+                    if ((1) == ((func0) + (2)))
+                        then
+                            (input) !! (2)
+                        else
+                            2
+                else
+                    2
+        func0 =
+            if ((1) == (1))
+                then
+                    1
+                else
+                    2
+"""
+
+    interpreter = HaskellInterpreter()
+    actual_code = interpreter.interpret(expr)
+    # dicts in Python 3.5 have nondeterministic order of keys
+    utils.assert_code_equal_any_of(
+        actual_code, expected_code_1, expected_code_2)
+
+
+def test_raw_array():
+    expr = ast.VectorVal([ast.NumVal(3), ast.NumVal(4)])
+
+    expected_code = """
+module Model where
+score :: [Double] -> [Double]
+score input =
+    [3, 4]
+"""
+
+    interpreter = HaskellInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_multi_output():
+    expr = ast.IfExpr(
+        ast.CompExpr(
+            ast.NumVal(1),
+            ast.NumVal(1),
+            ast.CompOpType.EQ),
+        ast.VectorVal([ast.NumVal(1), ast.NumVal(2)]),
+        ast.VectorVal([ast.NumVal(3), ast.NumVal(4)]))
+
+    expected_code = """
+module Model where
+score :: [Double] -> [Double]
+score input =
+    func0
+    where
+        func0 =
+            if ((1) == (1))
+                then
+                    [1, 2]
+                else
+                    [3, 4]
+"""
+
+    interpreter = HaskellInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_bin_vector_expr():
+    expr = ast.BinVectorExpr(
+        ast.VectorVal([ast.NumVal(1), ast.NumVal(2)]),
+        ast.VectorVal([ast.NumVal(3), ast.NumVal(4)]),
+        ast.BinNumOpType.ADD)
+
+    expected_code = """
+module Model where
+addVectors :: [Double] -> [Double] -> [Double]
+addVectors v1 v2 = zipWith (+) v1 v2
+mulVectorNumber :: [Double] -> Double -> [Double]
+mulVectorNumber v1 num = [i * num | i <- v1]
+score :: [Double] -> [Double]
+score input =
+    addVectors ([1, 2]) ([3, 4])
+"""
+
+    interpreter = HaskellInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_bin_vector_num_expr():
+    expr = ast.BinVectorNumExpr(
+        ast.VectorVal([ast.NumVal(1), ast.NumVal(2)]),
+        ast.NumVal(1),
+        ast.BinNumOpType.MUL)
+
+    expected_code = """
+module Model where
+addVectors :: [Double] -> [Double] -> [Double]
+addVectors v1 v2 = zipWith (+) v1 v2
+mulVectorNumber :: [Double] -> Double -> [Double]
+mulVectorNumber v1 num = [i * num | i <- v1]
+score :: [Double] -> [Double]
+score input =
+    mulVectorNumber ([1, 2]) (1)
+"""
+
+    interpreter = HaskellInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_exp_expr():
+    expr = ast.ExpExpr(ast.NumVal(1.0))
+
+    expected_code = """
+module Model where
+score :: [Double] -> Double
+score input =
+    exp (1.0)
+"""
+
+    interpreter = HaskellInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_pow_expr():
+    expr = ast.PowExpr(ast.NumVal(2.0), ast.NumVal(3.0))
+
+    expected_code = """
+module Model where
+score :: [Double] -> Double
+score input =
+    (2.0) ** (3.0)
+"""
+
+    interpreter = HaskellInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_sqrt_expr():
+    expr = ast.SqrtExpr(ast.NumVal(2.0))
+
+    expected_code = """
+module Model where
+score :: [Double] -> Double
+score input =
+    sqrt (2.0)
+"""
+
+    interpreter = HaskellInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_tanh_expr():
+    expr = ast.TanhExpr(ast.NumVal(2.0))
+
+    expected_code = """
+module Model where
+score :: [Double] -> Double
+score input =
+    tanh (2.0)
+"""
+
+    interpreter = HaskellInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
+
+
+def test_reused_expr():
+    reused_expr = ast.ExpExpr(ast.NumVal(1.0), to_reuse=True)
+    expr = ast.BinNumExpr(reused_expr, reused_expr, ast.BinNumOpType.DIV)
+
+    expected_code = """
+module Model where
+score :: [Double] -> Double
+score input =
+    (func0) / (func0)
+    where
+        func0 =
+            exp (1.0)
+"""
+
+    interpreter = HaskellInterpreter()
+    utils.assert_code_equal(interpreter.interpret(expr), expected_code)
diff --git a/tests/utils.py b/tests/utils.py
index af926a1e..d844c5ab 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -166,7 +166,11 @@ def cmp_exprs(left, right):
 
 
 def assert_code_equal(actual, expected):
-    assert actual.strip() == expected.strip()
+    actual.strip() == expected.strip()
+
+
+def assert_code_equal_any_of(actual, *expected):
+    assert any(actual.strip() == expect.strip() for expect in expected)
 
 
 get_regression_model_trainer = functools.partial(