From d867e8652098f127ac4ea5ee54301561bd1357fb Mon Sep 17 00:00:00 2001 From: Krzysztof Wende Date: Sat, 3 Feb 2018 16:47:00 +0100 Subject: [PATCH 1/3] roadmap --- roadmap/BASIC_TYPES.md | 27 +++++++++++++++++++ roadmap/COMMENTS.md | 53 ++++++++++++++++++++++++++++++++++++ roadmap/FUNCTIONS.md | 60 +++++++++++++++++++++++++++++++++++++++++ roadmap/INTEROP.md | 58 +++++++++++++++++++++++++++++++++++++++ roadmap/MODULES.md | 42 +++++++++++++++++++++++++++++ roadmap/README.md | 46 +++++++++++++++++++++++++++++++ roadmap/STRUCTURES.md | 58 +++++++++++++++++++++++++++++++++++++++ roadmap/TYPES.md | 56 ++++++++++++++++++++++++++++++++++++++ roadmap/TYPE_ALIASES.md | 36 +++++++++++++++++++++++++ 9 files changed, 436 insertions(+) create mode 100644 roadmap/BASIC_TYPES.md create mode 100644 roadmap/COMMENTS.md create mode 100644 roadmap/FUNCTIONS.md create mode 100644 roadmap/INTEROP.md create mode 100644 roadmap/MODULES.md create mode 100644 roadmap/README.md create mode 100644 roadmap/STRUCTURES.md create mode 100644 roadmap/TYPES.md create mode 100644 roadmap/TYPE_ALIASES.md diff --git a/roadmap/BASIC_TYPES.md b/roadmap/BASIC_TYPES.md new file mode 100644 index 0000000..f3a42b5 --- /dev/null +++ b/roadmap/BASIC_TYPES.md @@ -0,0 +1,27 @@ +# Basic types + + +Because Elm and Elixir share a lot of common basic types, there is no need to redefine all of them for Elchemy need. For simplicity and interoperability some of the standard types translate directly to each other. + +Here is a table of all standard types used in Elchemy environment and their Elixir equivalents: + +| Elchemy | Elixir | +| : | : | +| `a` | `any()` +| `comparable` | `term()` +| `Int` | `integer()` +| `Float` | `number()` +| `number` | `number()` +| `Bool` | `boolean()` +| `Char` | `integer()` +| `String` | `String.t()` +| `List x` | `list()` +| `(1, 2)` | `{1, 2}` +| `Maybe Int` | `{integer()}` | `nil` +| `Just x` | `{x}` +| `Nothing` | `nil` +| `Result x y` | `{:ok, y}` | `{:error, x}` +| `Ok x` | `{:ok, x}` +| `Err x` | `{:error, x}` +| `{x = 1, y = 1}` | `%{x: 1, y: y}` | +| `Dict String Int` | `%{key(String.t()) => integer())}` | diff --git a/roadmap/COMMENTS.md b/roadmap/COMMENTS.md new file mode 100644 index 0000000..2bd9ced --- /dev/null +++ b/roadmap/COMMENTS.md @@ -0,0 +1,53 @@ +# Comments + +In Elchemy there is several types of comments. + +### Inline comments +```elm +-- My comment +``` + +Which are always ignored by the compiler and won't be included in the compiled output file + +### Block comment + +Second type of a comment is a block comment + +``` elm +{- block comment -} +``` + +Which are included in the output file, but can only be used as a top level construct + +### Doc comments +Another one and probably most important is a doc-comment. + +``` elm +{-| Doc coment -} +``` +Doc-comments follow these rules: + +1. **First doc comment in a file always compiles to a `@moduledoc`** +2. Doc comments before types compile to `@typedoc` +3. Doc comments before functions compile to `@doc` +4. A doc comment with 4 space indentation and containing a `==` comparison in it will be compiled to a **one line** [doctest](https://elixir-lang.org/getting-started/mix-otp/docs-tests-and-with.html#doctests) + +Doctest example: + +```elm +{-| My function adds two values + + myFunction 1 2 == 3 +-} +``` + +Would end up being + +```elixir +@doc """ + My function adds two values + + iex> my_function().(1).(2) + 3 + """ +``` diff --git a/roadmap/FUNCTIONS.md b/roadmap/FUNCTIONS.md new file mode 100644 index 0000000..2eecd7f --- /dev/null +++ b/roadmap/FUNCTIONS.md @@ -0,0 +1,60 @@ +# Function definition and currying + +### Function definition +To define a function that gets exported you **need** to declare a type for it with + +``` elm +functionName : ArgType -> ArgType2 -> ReturnType +``` + +And a function body underneath + +``` elm +functionName argOne argTwo = body +``` + +This will output following definition: + +``` elixir +curry function_name/2 +@spec function_name(arg_type, arg_type2) :: return_type +def function_name(arg_one, arg_two) do + body +end +``` + +Following rules apply: + +1. A function will use `def` or `defp` based on `exposing` clause at the top of the module +2. The name of the function as well as its spec will always be snake_cased version of the camelCase name +3. `curry function/arity` is a construct that makes the function redefined in a form that takes 0 arguments, and returns X times curried function (2 times in this example). Which means that from elixir our function can be called both as: `function_name(arg_one, arg_two)` or `function_name().(arg_one).(arg_two)` and it won't have any different effect +4. `@spec` clause will always resolve types provided, to a most readable and still understandable by the elixir compiler form + +#### Curried definition +Because of the curried nature of Elm function definitions we can just make our function return functions + +For example instead of writing + +``` elm +addTwo : Int -> Int +addTwo a = 2 + a +``` + +We could just write + +``` elm +addTwo : Int -> Int +addTwo = (+) 2 +``` + +In which case Elchemy will recognize a curried return and still provide you with a 1 and 0 arity functions, not only the 0 one. +And output of such a definition would look like: + +``` elixir +curry add_two/1 +@spec add_two(integer) :: integer +def add_two(x1) do + (&+/0).(2).(x1) +end +``` +Which basically means that Elchemy derives function arity from the type, rathar then function body diff --git a/roadmap/INTEROP.md b/roadmap/INTEROP.md new file mode 100644 index 0000000..a10b270 --- /dev/null +++ b/roadmap/INTEROP.md @@ -0,0 +1,58 @@ +# Interop + + +## Calling Elchemy + +To call generated code from Elchemy you don't need anything special. +Each function exposed from a module can be called either in it's regular or curried form. + +Keep in mind that in case of functions expecting a [higher-order function\[s\]](https://en.wikipedia.org/wiki/Higher-order_function) in its parameters Elchemy will also do it's magic to make sure it's compatible both ways. + +For instance if your function looks like that: +```elm +applyFunction2 : (a -> b -> c) -> a -> b -> c +applyFunction2 f p1 p2 = f p1 p2 +``` + +You can call it Elixir way: +```elixir +apply_function2(fn a, b -> a + b end) +``` +As well as Elchemy (curried) way: +```elixir +apply_function2(fn a -> fn b -> a + b end end) +``` +And it will work as fine + + +--- +## Calling Elixir + + +To call elixir from Elchemy you need to define a foreing function interface. +To do that you can use `ffi` [special syntax (?)](SPECIAL_SYNTAX.md) + +`ffi` requires the function to have a very specific format, which is: + +1. You need to make sure the type signature is adequate to the corresponding typespec of a function +2. There should be no explicitly stated parameters (Defined as `f = ...` not `f a b c = ...`) +3. The **only** expression inside the function should be an ffi call in a format of: +`ffi "Module" "function"` + +A good example of an ffi call would be + +```elm + upcase : String -> String + upcase = + ffi "String" "upcase" +``` +A generated code of that statement would be + +``` elixir + @spec upcase(String.t) :: String.t + curry upcase/1 + verify as: String.upcase/1 + def upcase(a1), do: String.upcase(a1) +``` + +Where `verify, as:` is a type safety generator about which you can read more in [testing](TESTING.md) section. diff --git a/roadmap/MODULES.md b/roadmap/MODULES.md new file mode 100644 index 0000000..d192f1d --- /dev/null +++ b/roadmap/MODULES.md @@ -0,0 +1,42 @@ +# Module definition and imports + +To define a module in Elchemy you need to use a + +``` elm +module ModuleName exposing (..) +``` + +Which would directly translate to `defmodule` block where functions/types mentioned in the `exposing` clause will automatically use `def` or `defp` + +## Imports + +There are two types of imports in Elchemy. + +### Without exposing +One is a import without exposed functions like + +``` elm +import SomeModule +``` +Which would directly translate to + +``` elixir +alias SomeModule +``` + +Because it doesn't import any of the exposed contents, only makes sure +that the module is in our namespace. + +### With exposing + +``` elm +import SomeModule exposing (funA, TypeA, funB) +``` +Which outputs + +``` elixir +import SomeModule, only: [{:fun_a, 0}, {:fun_b, 0}] +``` + +Which would put `SomeModule` into our namespace and also allow us to use +`fun_a` and `fun_b` without explicitly adding a module name before them. diff --git a/roadmap/README.md b/roadmap/README.md new file mode 100644 index 0000000..717fbe7 --- /dev/null +++ b/roadmap/README.md @@ -0,0 +1,46 @@ +# Roadmap + +This folder lists all of the ideas and solutions behind Elchemy to share the +ideology as well as existing and incoming solutions to problems this project faced +or eventually will have to face + +# Table of contents +## Basics + - [Basic Types](BASIC_TYPES.md) + - [Defining Types](TYPES.md) + - [Defining Type Aliases](TYPE_ALIASES.md) + - [Structures](STRUCTURES.md) + - [Defining Modules](MODULES.md) + - [Defining Functions](FUNCTIONS.md) + - [Comments](COMMENTS.md) + - [Interop (Foreign Function Interface)](INTEROP.md) + +# Introduction + + +Elchemy is a set of tools and frameworks, designed to provide a language and an environment +as close to [Elm programming language](http://elm-lang.org) as possible, to build server applications +in a DSL-like manner for Erlang VM platform, with a readable and efficient Elixir code as an output. + +## Features + +Elchemy inherits many values from its parents: Elm and Elixir + +### Elm +- ML like syntax maximizing expressiveness with additional readability and simplicity constraints +- Static typing with type inference +- Beautiful compilation errors +- Tagged union types and type aliases with type parameters (aka generic types) +- All functions are curried by default +- [No typeclasses](http://www.haskellforall.com/2012/05/scrap-your-type-classes.html) + +### Erlang/Elixir +- Documentation as a first class citizen +- Doc tests +- Battle-tested distribution system that just works + +### Additional +- Foreign function calls type saftety +- Foreign function calls purity checks +- Dependency system based on GitHub +- Compile time code optimizations diff --git a/roadmap/STRUCTURES.md b/roadmap/STRUCTURES.md new file mode 100644 index 0000000..eb5e01c --- /dev/null +++ b/roadmap/STRUCTURES.md @@ -0,0 +1,58 @@ +# Structs + +Elchemy represents all the structs as maps, so a struct defined like + +``` elm +human : { name : String + , age : Int + } +``` +Is an equivalent of + +``` elixir +@spec human :: %{name: String.t(), age: integer()} +``` + +Also type aliases denoting structs can be instantiated like functions + +``` elm +type alias Human = + { name : String + , age : Int + } +``` +``` elm +Human "Krzysztof" 22 +``` + +## Struct polymorphism +What's more structs can describe a map that has at least specified elements using an update syntax. + +``` elm +type alias Employee x = + { x + | salary : Int + } +``` +Which means any struct that has a field `salary` of type integer. +That way we can define our psuedo-inheritance and polymorphism for more advanced structures. + +``` elm +type alias Human = + Employee + { name : String + , age : Int } + +human : Human +``` +Would resolve to + +``` elixir +@spec human :: %{ + salary: integer(), + name: String.t, + age: integer() +} +``` + +But be advised that using this "polymorhpic" approach strips us from the ability to use type aliases as constructors. diff --git a/roadmap/TYPES.md b/roadmap/TYPES.md new file mode 100644 index 0000000..4507bb0 --- /dev/null +++ b/roadmap/TYPES.md @@ -0,0 +1,56 @@ +# Union types + +Elchemy (exactly like Elm) uses [Tagged Union types](https://en.wikipedia.org/wiki/Tagged_union) +What it means is basically you can define a type by adding a tag to +an already existing value and a meaning of tag is only to inform what is the +context of that value. + +For instance + +``` elm +type Shape = Dot Int | Line Int Int | Triangle Int Int Int +``` +What's important is that Dot, Line and Triangle are just tags, so they can't be used as a type name (in function signature for example) +The only purpose of these is to pattern match on them in constructs like case..of, let..in or in arguments + +Elchemy represents tagged unions as tuples with a first element being an atom with snake_cased tag name, or - in case of just tags - as a single atom value. + +For example our previously defined type would translate to + +``` elixir +@type shape :: { :dot, integer() } | + { :line, integer(), integer() } | + { :triangle, integer(), integer(), integer() } +``` +But a type like this + +``` elm +type Size = XS | S | M | L | XL +``` +Would translate to + +``` elixir +@type size :: :x | :s | :m | :l | :xl +``` +### Type Parameters + +Types can also take type parameters like + +``` elm +type Maybe x = Just x | Nothing +``` +All type parameters will resolve to any() by Elchemy + +### Types as constructors + +Types can be instantiated using a tag and values it takes. +For example to instantiate `Just Int` we would write `Just 10`. +If you don't provide all of the parameters, Elchemy will recognize it and +translate it into a curried function, so that `Just` instead of turning to +``` elixir +:just +``` +it turns to +``` elixir +fn x1 -> {:just, x1} end +``` diff --git a/roadmap/TYPE_ALIASES.md b/roadmap/TYPE_ALIASES.md new file mode 100644 index 0000000..4259253 --- /dev/null +++ b/roadmap/TYPE_ALIASES.md @@ -0,0 +1,36 @@ +# Type Aliases + +In Elchemy Type Aliases are completely virtual constructs that never make it out of the compiler. +However whenever a type alias is used throughout your code Elchemy will expand the alias and substitute with the right replacement. + +For instance if we write + +``` elm +type alias MyList = List Int + +a : MyList +a = [1, 2, 3] +``` + +The Elixir output would be + +``` elixir +@spec a :: list(integer()) +def a(), do: [1, 2, 3] +``` +With correct type resolution + + +## Type aliases as constructors + +If a type alias represents a structure like + +``` elm +type alias Human = { name : String, age : Int } +``` +You can use the name of an alias as a function to quickly instatiate a struct +. For instance: + +``` elm +Human "Krzysztof" 22 +``` From 643b7119d077a97f81b1f560605418a4e5660989 Mon Sep 17 00:00:00 2001 From: Krzysztof Wende Date: Sat, 3 Feb 2018 16:48:57 +0100 Subject: [PATCH 2/3] Update BASIC_TYPES.md --- roadmap/BASIC_TYPES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roadmap/BASIC_TYPES.md b/roadmap/BASIC_TYPES.md index f3a42b5..9f9952a 100644 --- a/roadmap/BASIC_TYPES.md +++ b/roadmap/BASIC_TYPES.md @@ -6,7 +6,7 @@ Because Elm and Elixir share a lot of common basic types, there is no need to re Here is a table of all standard types used in Elchemy environment and their Elixir equivalents: | Elchemy | Elixir | -| : | : | +| --- | --- | | `a` | `any()` | `comparable` | `term()` | `Int` | `integer()` From 9d4de4df67bb2f10337dbddc870b026974f63bb1 Mon Sep 17 00:00:00 2001 From: Krzysztof Wende Date: Sat, 3 Feb 2018 23:18:03 +0100 Subject: [PATCH 3/3] New documentation files --- roadmap/FLAGS.md | 45 +++++++++++++++++++++++++ roadmap/INLINING.md | 17 ++++++++++ roadmap/INSTALLATION.md | 67 ++++++++++++++++++++++++++++++++++++++ roadmap/README.md | 12 ++++++- roadmap/SIDE_EFFECTS.md | 3 ++ roadmap/TESTING.md | 4 +++ roadmap/TROUBLESHOOTING.md | 28 ++++++++++++++++ 7 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 roadmap/FLAGS.md create mode 100644 roadmap/INLINING.md create mode 100644 roadmap/INSTALLATION.md create mode 100644 roadmap/SIDE_EFFECTS.md create mode 100644 roadmap/TESTING.md create mode 100644 roadmap/TROUBLESHOOTING.md diff --git a/roadmap/FLAGS.md b/roadmap/FLAGS.md new file mode 100644 index 0000000..5e2294d --- /dev/null +++ b/roadmap/FLAGS.md @@ -0,0 +1,45 @@ +# Flags + +Elchemy compiler for purposes of experimenting and stretching the boundaries accepts flags. + +#### DO NOT ATTEMPT TO DO THAT UNLESS YOU'RE SURE IT'S THE ONLY WAY + +To pass a flag to a compiler there is a special comment syntax + +``` +{- flag flagname:+argument flagname2:+argument2 } +``` + +So far there is 4 flag types: +#### `notype:+TypeName` +Omits putting the `@type` into the compiled output code +Used when you need type checking inside Elchemy ecosystem, without forwarding the definition into the output code. +Example: +```elm +{- flag notype:+MyHiddenType -} +type MyHiddenType = Hidden a +``` +--- +#### `nodef:+functionName` +Omits entire function definition from the code output. The function `@spec` will still be produced. +Example: +```elm +{- flag nodef:+myHiddenFunction -} +myHiddenFunction = 1 +``` +--- +#### `nospec:+functionName` +Omits function spec from the code output +Example: +```elm +{- flag nospec:+myHiddenFunction -} +myHiddenFunction : Int +``` +--- +#### `noverify:+functionName` +Omits function verify macro from the code output. Usable only when using for functions defined as FFI +Example: +```elm +{- flag nospec:+myHiddenFunction -} +myHiddenFunction : Int +``` diff --git a/roadmap/INLINING.md b/roadmap/INLINING.md new file mode 100644 index 0000000..6fcf453 --- /dev/null +++ b/roadmap/INLINING.md @@ -0,0 +1,17 @@ +# Inlining Elixir Code + + +#### DO NOT ATTEMPT TO DO THAT UNLESS YOU'RE SURE IT'S THE ONLY WAY + +In Elchemy it's possible to inline Elixir code using + +```elm +{- ex + +## Code +def any_function() do + 1 +end + +-} +``` diff --git a/roadmap/INSTALLATION.md b/roadmap/INSTALLATION.md new file mode 100644 index 0000000..32c9ea1 --- /dev/null +++ b/roadmap/INSTALLATION.md @@ -0,0 +1,67 @@ +# Installation + +You can install Elchemy using [npm](https://www.npmjs.com/) with +``` +npm install -g elchemy +``` + +To integrate Elchemy with your project you need to execute: + +``` +elchemy init +``` + +Inside your elixir project directory. +If you don't have a project created, you need to first create it. It's advised to use [Mix](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html#our-first-project) for that. + +Assuming the simplest example project called `my_project` the standard path would be: + +``` +mix new my_project +cd my_project +elchemy init +``` + +Then open your `mix.exs` file inside project root directory. And add +```elixir +|> Code.eval_file("elchemy.exs").init +``` +At the end of your `project/0` function definition. Like so: +Before: +```elixir +defmodule MyProject.Mixfile do + use Mix.Project + + def project do + [ + app: :my_project, + version: "0.1.0", + elixir: "~> 1.5", + start_permanent: Mix.env == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + ... +``` +After: +```elixir +defmodule MyProject.Mixfile do + use Mix.Project + + def project do + [ + app: :my_project, + version: "0.1.0", + elixir: "~> 1.5", + start_permanent: Mix.env == :prod, + deps: deps() + ] |> Code.eval_file("elchemy.exs").init + end + + # Run "mix help compile.app" to learn about applications. + def application do + ... +``` diff --git a/roadmap/README.md b/roadmap/README.md index 717fbe7..2910a58 100644 --- a/roadmap/README.md +++ b/roadmap/README.md @@ -15,6 +15,16 @@ or eventually will have to face - [Comments](COMMENTS.md) - [Interop (Foreign Function Interface)](INTEROP.md) +## Advanced + - [Side Effects / TEA](SIDE_EFFECTS.md) + - [Unit Testing](TESTING.md) + - [Compiler flags](FLAGS.md) + - [Inlining Elixir](INLINING.md) + +## Tooling + - [Installation](INSTALLATION.md) + - [Troubleshooting](TROUBLESHOOTING.md) + # Introduction @@ -40,7 +50,7 @@ Elchemy inherits many values from its parents: Elm and Elixir - Battle-tested distribution system that just works ### Additional -- Foreign function calls type saftety +- Foreign function calls type safety - Foreign function calls purity checks - Dependency system based on GitHub - Compile time code optimizations diff --git a/roadmap/SIDE_EFFECTS.md b/roadmap/SIDE_EFFECTS.md new file mode 100644 index 0000000..4f9be83 --- /dev/null +++ b/roadmap/SIDE_EFFECTS.md @@ -0,0 +1,3 @@ +# Side Effects + +It's currently impossible to express side effects using Elchemy. diff --git a/roadmap/TESTING.md b/roadmap/TESTING.md new file mode 100644 index 0000000..488083f --- /dev/null +++ b/roadmap/TESTING.md @@ -0,0 +1,4 @@ +# Testing + +So far there is no unit testing tool for Elchemy. +To test your code use Elixir's `ExUnit` or see document [COMMENTS.md](COMMENTS.md) for using Doctests. diff --git a/roadmap/TROUBLESHOOTING.md b/roadmap/TROUBLESHOOTING.md new file mode 100644 index 0000000..e21d5c5 --- /dev/null +++ b/roadmap/TROUBLESHOOTING.md @@ -0,0 +1,28 @@ +# Troubleshooting + +Generally grand amount of problems can be solved with a simple command: +``` +elchemy clean +``` + +It cleans all of the caches, temporary files, dependencies and code file outputs out of the current project. + +If +``` +elchemy clean +elchemy init +mix compile +``` + +still yields compilation errors feel free to report an issue. +For the sake of being able to reproduce please provide: + +- Elchemy version (`elchemy version`) +- Elixir version (`elixir -v`) +- Erlang version (first line in `erl`) +- Elm version (`elm -v`) +- Operating system (`uname -msr`) + +It's also helpful to include: +- Result of `tree -L 2` inside your project folder +- Code of the file failing