├── .credo.exs ├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── color.ex ├── either.ex ├── maybe.ex ├── pair.ex ├── params_and_concrete.ex ├── person.ex ├── person_record.ex └── tree.ex ├── lib ├── haex.ex └── haex │ ├── ast.ex │ ├── data.ex │ └── data │ ├── data_builder.ex │ ├── data_constructor.ex │ ├── data_constructor_builder.ex │ ├── parser.ex │ ├── type_constructor.ex │ └── type_constructor_builder.ex ├── mix.exs ├── mix.lock └── test ├── data_test.exs └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: [ 68 | # 69 | ## Consistency Checks 70 | # 71 | {Credo.Check.Consistency.ExceptionNames, []}, 72 | {Credo.Check.Consistency.LineEndings, []}, 73 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 74 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 75 | {Credo.Check.Consistency.SpaceInParentheses, []}, 76 | {Credo.Check.Consistency.TabsOrSpaces, []}, 77 | 78 | # 79 | ## Design Checks 80 | # 81 | # You can customize the priority of any check 82 | # Priority values are: `low, normal, high, higher` 83 | # 84 | {Credo.Check.Design.AliasUsage, 85 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 86 | # You can also customize the exit_status of each check. 87 | # If you don't want TODO comments to cause `mix credo` to fail, just 88 | # set this value to 0 (zero). 89 | # 90 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 91 | {Credo.Check.Design.TagFIXME, []}, 92 | 93 | # 94 | ## Readability Checks 95 | # 96 | {Credo.Check.Readability.AliasOrder, []}, 97 | {Credo.Check.Readability.FunctionNames, []}, 98 | {Credo.Check.Readability.LargeNumbers, []}, 99 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 100 | {Credo.Check.Readability.ModuleAttributeNames, []}, 101 | {Credo.Check.Readability.ModuleDoc, []}, 102 | {Credo.Check.Readability.ModuleNames, []}, 103 | {Credo.Check.Readability.ParenthesesInCondition, []}, 104 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, false}, 105 | {Credo.Check.Readability.PredicateFunctionNames, []}, 106 | {Credo.Check.Readability.PreferImplicitTry, []}, 107 | {Credo.Check.Readability.RedundantBlankLines, []}, 108 | {Credo.Check.Readability.Semicolons, []}, 109 | {Credo.Check.Readability.SpaceAfterCommas, []}, 110 | {Credo.Check.Readability.StringSigils, []}, 111 | {Credo.Check.Readability.TrailingBlankLine, []}, 112 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 113 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 114 | {Credo.Check.Readability.VariableNames, []}, 115 | 116 | # 117 | ## Refactoring Opportunities 118 | # 119 | {Credo.Check.Refactor.CondStatements, []}, 120 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 121 | {Credo.Check.Refactor.FunctionArity, []}, 122 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 123 | # {Credo.Check.Refactor.MapInto, []}, 124 | {Credo.Check.Refactor.MatchInCondition, []}, 125 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 126 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 127 | {Credo.Check.Refactor.Nesting, []}, 128 | {Credo.Check.Refactor.UnlessWithElse, []}, 129 | {Credo.Check.Refactor.WithClauses, []}, 130 | 131 | # 132 | ## Warnings 133 | # 134 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 135 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 136 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 137 | {Credo.Check.Warning.IExPry, []}, 138 | {Credo.Check.Warning.IoInspect, []}, 139 | # {Credo.Check.Warning.LazyLogging, []}, 140 | {Credo.Check.Warning.MixEnv, false}, 141 | {Credo.Check.Warning.OperationOnSameValues, []}, 142 | {Credo.Check.Warning.OperationWithConstantResult, []}, 143 | {Credo.Check.Warning.RaiseInsideRescue, []}, 144 | {Credo.Check.Warning.UnusedEnumOperation, []}, 145 | {Credo.Check.Warning.UnusedFileOperation, []}, 146 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 147 | {Credo.Check.Warning.UnusedListOperation, []}, 148 | {Credo.Check.Warning.UnusedPathOperation, []}, 149 | {Credo.Check.Warning.UnusedRegexOperation, []}, 150 | {Credo.Check.Warning.UnusedStringOperation, []}, 151 | {Credo.Check.Warning.UnusedTupleOperation, []}, 152 | {Credo.Check.Warning.UnsafeExec, []}, 153 | 154 | # 155 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 156 | 157 | # 158 | # Controversial and experimental checks (opt-in, just replace `false` with `[]`) 159 | # 160 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 161 | {Credo.Check.Consistency.UnusedVariableNames, false}, 162 | {Credo.Check.Design.DuplicatedCode, false}, 163 | {Credo.Check.Readability.AliasAs, false}, 164 | {Credo.Check.Readability.BlockPipe, false}, 165 | {Credo.Check.Readability.ImplTrue, false}, 166 | {Credo.Check.Readability.MultiAlias, false}, 167 | {Credo.Check.Readability.SeparateAliasRequire, false}, 168 | {Credo.Check.Readability.SinglePipe, false}, 169 | {Credo.Check.Readability.Specs, false}, 170 | {Credo.Check.Readability.StrictModuleLayout, false}, 171 | {Credo.Check.Readability.WithCustomTaggedTuple, false}, 172 | {Credo.Check.Refactor.ABCSize, false}, 173 | {Credo.Check.Refactor.AppendSingleItem, false}, 174 | {Credo.Check.Refactor.DoubleBooleanNegation, false}, 175 | {Credo.Check.Refactor.ModuleDependencies, false}, 176 | {Credo.Check.Refactor.NegatedIsNil, false}, 177 | {Credo.Check.Refactor.PipeChainStart, false}, 178 | {Credo.Check.Refactor.VariableRebinding, false}, 179 | {Credo.Check.Warning.LeakyEnvironment, false}, 180 | {Credo.Check.Warning.MapGetUnsafePass, false}, 181 | {Credo.Check.Warning.UnsafeToAtom, false} 182 | 183 | # 184 | # Custom checks can be created using `mix credo.gen.check`. 185 | # 186 | ] 187 | } 188 | ] 189 | } 190 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [data: :*], 5 | export: [ 6 | locals_without_parens: [data: :*] 7 | ] 8 | ] 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | haex-*.tar 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Julian Doherty 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Haex 2 | 3 | _"Haskell as Elixir"_ - A DSL for implementing Haskell style sum and product data types in Elixir 4 | 5 | _(pronounced "hacks")_ 6 | 7 | A succinct statement generates a number of modules, and helper functions, 8 | along with valid dialyzer type and specs to work with those data structures. 9 | 10 | The goal is to make it quicker and easier to work with rich types without 11 | having to implement a lot of boilerplate code to build common patterns. This 12 | is particularly true in the case of Elixir structs, which to be used 13 | properly, require a `defstruct` call, `@type` declaration (which is _almost 14 | but not quite_ identical), and an `@enforce_keys` annotaiton. All of these 15 | can be automated away. 16 | 17 | A secondary goal is to encourage good use of types that are understand by 18 | dialzyer. This makes it easier to work with and reason about the code, and 19 | aids documentation of what functions expect and return. 20 | ### Basics 21 | ```elixir 22 | import Haex 23 | 24 | data Maybe.t(a) :: Nothing | Just.t(a) 25 | 26 | Maybe.just("cheese") 27 | # {Maybe.Just, "cheese"} 28 | 29 | Maybe.Just.new("cheese") 30 | # {Maybe.Just, "cheese"} 31 | 32 | Maybe.nothing() 33 | # Maybe.Nothing 34 | 35 | Maybe.Nothing.new() 36 | # Maybe.Nothing 37 | ``` 38 | 39 | When compiled from file, and loaded in iex... 40 | 41 | ```elixir 42 | iex(1)> t Maybe 43 | @type t(a) :: Maybe.Nothing.t() | Maybe.Just.t(a) 44 | 45 | iex(2) h Maybe.just 46 | def just(arg1) 47 | @spec just(a) :: t(a) when a: var 48 | 49 | iex(3) h Maybe.nothing 50 | def nothing() 51 | @spec nothing() :: t(_a) 52 | ``` 53 | 54 | This one line statement (`Maybe.t(a) :: Nothing | Just.t(a)`) generated code that looks something like... 55 | ```elixir 56 | defmodule Maybe do 57 | @type t(a) :: Maybe.Nothing.t() | Maybe.Just.t(a) 58 | 59 | defmodule Nothing do 60 | @opaque t() :: __MODULE__ 61 | 62 | @spec(new() :: t()) 63 | def new() do 64 | __MODULE__ 65 | end 66 | end 67 | 68 | defmodule Just do 69 | @opaque t(a) :: {__MODULE__, a} 70 | 71 | @spec(new(a) :: t(a) when a: var) 72 | def new(arg1) do 73 | {__MODULE__, arg1} 74 | end 75 | end 76 | 77 | @spec nothing() :: t(_a) when _a: var 78 | def nothing() do 79 | Nothing.new() 80 | end 81 | 82 | @spec just(a) :: t(a) when a: var) 83 | def just(arg1) do 84 | Just.new(arg1) 85 | end 86 | end 87 | ``` 88 | 89 | ...saving you a lot of keyboard wear 90 | 91 | ## Enum Types 92 | ```elixir 93 | data Color :: Red | Green | Blue | BlueSteel 94 | ``` 95 | 96 | ## Sum Types 97 | ```elixir 98 | data SocialMediaAccount :: 99 | Twitok.t(String.t()) 100 | | Facepalm.t(String.t()) 101 | | Watzap.t(String.t()) 102 | | Instaban.t(String.t()) 103 | ``` 104 | 105 | ## Product Types 106 | ```elixir 107 | data Pair.t(a, b) :: Pair.t(a, b) 108 | 109 | data Either.t(a, b) :: Left.t(a) | Right.t(b) 110 | 111 | data Person:: 112 | Person.t( 113 | name: String.t(), 114 | social_media: [SocialMediaAccount.t()], 115 | age: integer(), 116 | height: float(), 117 | favourite_ice_cream: String.t(), 118 | standard_quote: String.t() 119 | ) 120 | ``` 121 | ## Installation 122 | 123 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 124 | by adding `haex` to your list of dependencies in `mix.exs`: 125 | 126 | ```elixir 127 | def deps do 128 | [ 129 | {:haex, "~> 0.0.1"} 130 | ] 131 | end 132 | ``` 133 | 134 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 135 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 136 | be found at [https://hexdocs.pm/haex](https://hexdocs.pm/haex). 137 | 138 | -------------------------------------------------------------------------------- /examples/color.ex: -------------------------------------------------------------------------------- 1 | import Haex 2 | 3 | # data Color = Red | Green | Blue | BlueSteel 4 | data Color :: Red | Green | Blue | BlueSteel 5 | -------------------------------------------------------------------------------- /examples/either.ex: -------------------------------------------------------------------------------- 1 | import Haex 2 | 3 | # data Either a b = Left a | Right b 4 | data Either.t(a, b) :: Left.t(a) | Right.t(b) 5 | -------------------------------------------------------------------------------- /examples/maybe.ex: -------------------------------------------------------------------------------- 1 | import Haex 2 | 3 | # data Maybe a = Nothing | Just a 4 | data Maybe.t(a) :: Nothing | Just.t(a) 5 | -------------------------------------------------------------------------------- /examples/pair.ex: -------------------------------------------------------------------------------- 1 | import Haex 2 | 3 | # data Pair a b = Pair a b 4 | data Pair.t(a, b) :: Pair.t(a, b) 5 | -------------------------------------------------------------------------------- /examples/params_and_concrete.ex: -------------------------------------------------------------------------------- 1 | import Haex 2 | 3 | data Foo.t(a) :: Bar.t(a, integer()) | Baz.t(boolean(), integer()) 4 | -------------------------------------------------------------------------------- /examples/person.ex: -------------------------------------------------------------------------------- 1 | import Haex 2 | 3 | # data Person = Person String String Int Float String String 4 | data Person :: Person.t(String.t(), String.t(), integer(), float(), String.t(), String.t()) 5 | -------------------------------------------------------------------------------- /examples/person_record.ex: -------------------------------------------------------------------------------- 1 | import Haex 2 | 3 | # data SocialMedia = Twitok String | Facepalm String | Watzap String | Instaban String 4 | data SocialMedia :: 5 | Twitok.t(String.t()) 6 | | Facepalm.t(String.t()) 7 | | Watzap.t(String.t()) 8 | | Instaban.t(String.t()) 9 | 10 | data PersonRec :: 11 | PersonRec.t( 12 | name: String.t(), 13 | social_media: [SocialMedia.t()], 14 | age: integer(), 15 | height: float(), 16 | favourite_ice_cream: String.t(), 17 | standard_quote: String.t() 18 | ) 19 | -------------------------------------------------------------------------------- /examples/tree.ex: -------------------------------------------------------------------------------- 1 | import Haex 2 | 3 | # data Tree a = Leaf | Node a (Tree a) (Tree a) 4 | data Tree.t(a) :: Leaf | Node.t(a, Tree.t(a), Tree.t(a)) 5 | -------------------------------------------------------------------------------- /lib/haex.ex: -------------------------------------------------------------------------------- 1 | defmodule Haex do 2 | @moduledoc """ 3 | Haskell As Elixir - Haex (pronounced "hacks") 4 | 5 | Using Elixir's macro system to define a DSL that _sort of_ looks like 6 | Haskell's syntax for succinctly defining data types 7 | """ 8 | 9 | @spec data(Macro.t()) :: Macro.output() 10 | defmacro data(ast) do 11 | ast 12 | |> Haex.Data.data() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/haex/ast.ex: -------------------------------------------------------------------------------- 1 | defmodule Haex.Ast do 2 | @moduledoc """ 3 | utility functions to generate common AST forms used in multiple places 4 | """ 5 | alias Haex.Data 6 | 7 | @spec mod(Data.mod_name()) :: Macro.output() 8 | def mod(name), do: {:__aliases__, [alias: false], name} 9 | 10 | @spec or_pipe_join([Macro.output()]) :: Macro.output() 11 | def or_pipe_join([ast]) do 12 | ast 13 | end 14 | 15 | def or_pipe_join([ast | asts]) do 16 | {:|, [], [ast, or_pipe_join(asts)]} 17 | end 18 | 19 | @spec macro_puts(Macro.output()) :: Macro.output() 20 | def macro_puts(ast) do 21 | ast 22 | |> Macro.to_string() 23 | |> IO.puts() 24 | 25 | ast 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/haex/data.ex: -------------------------------------------------------------------------------- 1 | defmodule Haex.Data do 2 | @moduledoc """ 3 | Parses and builds modules to implement data types 4 | """ 5 | 6 | alias Haex.Data.DataBuilder 7 | alias Haex.Data.DataConstructor 8 | alias Haex.Data.Parser 9 | alias Haex.Data.TypeConstructor 10 | 11 | @type t() :: %__MODULE__{ 12 | type_constructor: TypeConstructor.t(), 13 | data_constructors: [DataConstructor.t()] 14 | } 15 | @type mod_name() :: [atom()] 16 | 17 | @type param_name() :: atom() 18 | @type param() :: {:variable, param_name()} | {:external_type, raw_ast :: term()} 19 | @type param_keywords() :: [{param_name(), param()}] 20 | 21 | @enforce_keys [:type_constructor, :data_constructors] 22 | defstruct [:type_constructor, :data_constructors] 23 | 24 | @spec data(Macro.t()) :: Macro.output() 25 | def data(data_ast) do 26 | data_ast 27 | |> Parser.parse() 28 | |> DataBuilder.build() 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/haex/data/data_builder.ex: -------------------------------------------------------------------------------- 1 | defmodule Haex.Data.DataBuilder do 2 | @moduledoc """ 3 | generates AST representation of `Haex.Data` to return from `Haex.data/1` macro 4 | """ 5 | 6 | alias Haex.Data 7 | alias Haex.Data.TypeConstructorBuilder 8 | 9 | @spec build(Data.t()) :: Macro.output() 10 | def build(%Data{type_constructor: type_constructor, data_constructors: data_constructors}) do 11 | TypeConstructorBuilder.build(type_constructor, data_constructors) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/haex/data/data_constructor.ex: -------------------------------------------------------------------------------- 1 | defmodule Haex.Data.DataConstructor do 2 | @moduledoc """ 3 | Builds data constructor modules used to implemente a data type 4 | """ 5 | 6 | alias __MODULE__, as: T 7 | alias Haex.Data 8 | 9 | @type t() :: %T{ 10 | name: Data.mod_name(), 11 | params: [Data.param()] | Data.param_keywords(), 12 | record?: boolean() 13 | } 14 | @enforce_keys [:name, :params, :record?] 15 | defstruct [:name, :params, :record?] 16 | 17 | @spec type_variables(t()) :: [Data.param()] 18 | def type_variables(%T{params: params, record?: false}) do 19 | params 20 | |> Enum.filter(fn {param_type, _var} -> param_type == :variable end) 21 | |> Enum.uniq() 22 | end 23 | 24 | def type_variables(%T{params: params, record?: true}) do 25 | params 26 | |> Enum.filter(fn {_param_name, {param_type, _var}} -> param_type == :variable end) 27 | |> Enum.uniq() 28 | end 29 | 30 | @spec has_variable?(t(), Data.param_name()) :: boolean() 31 | def has_variable?(%T{params: params}, variable_name) do 32 | Enum.any?(params, fn {param_type, param_name} -> 33 | param_type == :variable && param_name == variable_name 34 | end) 35 | end 36 | 37 | def helper_name(%T{name: name}) do 38 | name 39 | |> List.last() 40 | |> Atom.to_string() 41 | |> Macro.underscore() 42 | |> String.to_atom() 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/haex/data/data_constructor_builder.ex: -------------------------------------------------------------------------------- 1 | defmodule Haex.Data.DataConstructorBuilder do 2 | @moduledoc """ 3 | generates AST representation of `Haex.Data.DataConstructor` to return from 4 | `Haex.data/1` macro 5 | """ 6 | alias Haex.Ast 7 | alias Haex.Data 8 | alias Haex.Data.DataConstructor 9 | alias Haex.Data.TypeConstructor 10 | 11 | @spec build(DataConstructor.t()) :: Macro.output() 12 | def build(%DataConstructor{name: name} = dc) do 13 | quote do 14 | defmodule unquote(Ast.mod(name)) do 15 | unquote(type_spec(dc)) 16 | unquote(type_struct(dc)) 17 | unquote(new(dc)) 18 | end 19 | end 20 | end 21 | 22 | @spec when_clause(DataConstructor.t()) :: Macro.output() 23 | defp when_clause(%DataConstructor{} = dc) do 24 | dc 25 | |> DataConstructor.type_variables() 26 | |> Enum.map(fn {:variable, variable} -> {variable, {:var, [], Elixir}} end) 27 | end 28 | 29 | @spec type_fields(DataConstructor.t()) :: Macro.output() 30 | def type_fields(%DataConstructor{params: params}) do 31 | Enum.map(params, ¶m_to_typespec_param/1) 32 | end 33 | 34 | @spec type_spec(DataConstructor.t()) :: Macro.output() 35 | defp type_spec(%DataConstructor{params: []} = dc) do 36 | type_t = type_t(dc) 37 | 38 | quote do 39 | @opaque unquote(type_t) :: __MODULE__ 40 | end 41 | end 42 | 43 | defp type_spec(%DataConstructor{record?: false, params: params} = dc) when params != [] do 44 | type_t = type_t(dc) 45 | type_fields = type_fields(dc) 46 | 47 | quote do 48 | @opaque unquote(type_t) :: {__MODULE__, unquote_splicing(type_fields)} 49 | end 50 | end 51 | 52 | defp type_spec(%DataConstructor{record?: true, params: params} = dc) when params != [] do 53 | type_t = type_t(dc) 54 | type_fields = type_fields(dc) 55 | 56 | quote do 57 | @opaque unquote(type_t) :: %__MODULE__{unquote_splicing(type_fields)} 58 | end 59 | end 60 | 61 | defp type_struct(%DataConstructor{record?: true} = dc) do 62 | struct_fields = 63 | dc 64 | |> type_fields() 65 | |> Enum.map(fn {name, _field} -> {name, nil} end) 66 | 67 | enforce_keys = 68 | dc 69 | |> type_fields() 70 | |> Enum.map(fn {name, _field} -> name end) 71 | 72 | quote do 73 | @enforce_keys unquote(enforce_keys) 74 | defstruct unquote(struct_fields) 75 | end 76 | end 77 | 78 | defp type_struct(%DataConstructor{record?: false}) do 79 | quote do 80 | end 81 | end 82 | 83 | @spec new(DataConstructor.t()) :: Macro.output() 84 | defp new(%DataConstructor{params: []} = dc) do 85 | type_t = type_t(dc) 86 | 87 | quote do 88 | @spec new() :: unquote(type_t) 89 | def new(), do: __MODULE__ 90 | end 91 | end 92 | 93 | defp new(%DataConstructor{record?: false} = dc) do 94 | type_fields = type_fields(dc) 95 | type_t = type_t(dc) 96 | when_clause = when_clause(dc) 97 | args = Macro.generate_arguments(length(type_fields), nil) 98 | 99 | quote do 100 | @spec new(unquote_splicing(type_fields)) :: unquote(type_t) 101 | when unquote(when_clause) 102 | def new(unquote_splicing(args)), do: {__MODULE__, unquote_splicing(args)} 103 | end 104 | end 105 | 106 | defp new(%DataConstructor{record?: true} = dc) do 107 | type_fields = type_fields(dc) 108 | 109 | type_field_args = 110 | type_fields 111 | |> Enum.map(fn {name, type} -> quote(do: unquote({name, [], Elixir}) :: unquote(type)) end) 112 | 113 | type_field_names = type_fields |> Enum.map(fn {name, _field} -> name end) 114 | type_t = type_t(dc) 115 | when_clause = when_clause(dc) 116 | args = Enum.map(type_field_names, fn name -> {name, [], Elixir} end) 117 | 118 | struct_args = 119 | type_field_names 120 | |> Enum.zip(args) 121 | 122 | quote do 123 | @spec new(unquote_splicing(type_field_args)) :: unquote(type_t) 124 | when unquote(when_clause) 125 | def new(unquote_splicing(args)), do: %__MODULE__{unquote_splicing(struct_args)} 126 | end 127 | end 128 | 129 | @spec qualified_type_t(TypeConstructor.t(), DataConstructor.t()) :: Macro.output() 130 | def qualified_type_t(%TypeConstructor{name: tc_name}, %DataConstructor{name: name} = dc) do 131 | mod = Ast.mod(tc_name ++ name) 132 | type_t = type_t(dc) 133 | 134 | quote do 135 | unquote(mod).unquote(type_t) 136 | end 137 | end 138 | 139 | @spec type_t(DataConstructor.t()) :: Macro.output() 140 | defp type_t(%DataConstructor{params: []}) do 141 | quote do 142 | t() 143 | end 144 | end 145 | 146 | defp type_t(%DataConstructor{params: params} = dc) when params != [] do 147 | quoted_type_variables = 148 | dc 149 | |> DataConstructor.type_variables() 150 | |> Enum.map(¶m_to_typespec_param/1) 151 | 152 | quote do 153 | t(unquote_splicing(quoted_type_variables)) 154 | end 155 | end 156 | 157 | @spec param_to_typespec_param(Data.param()) :: Macro.output() 158 | defp param_to_typespec_param({:variable, variable}), do: {variable, [], Elixir} 159 | defp param_to_typespec_param({:external_type, external}), do: external 160 | 161 | defp param_to_typespec_param({param_name, {param_type, param_ast}}), 162 | do: {param_name, param_to_typespec_param({param_type, param_ast})} 163 | end 164 | -------------------------------------------------------------------------------- /lib/haex/data/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule Haex.Data.Parser do 2 | @moduledoc """ 3 | Parses an AST received from `Haex.data/1` macro, and generates a 4 | `Haex.Data.t()` struct which can be used to generate modules to represent 5 | that data type 6 | """ 7 | alias Haex.Data 8 | alias Haex.Data.DataConstructor 9 | alias Haex.Data.TypeConstructor 10 | 11 | @spec parse(Macro.t()) :: Data.t() 12 | def parse({:"::", _meta, [type_ast, data_asts]}) do 13 | type_constructor = parse_type_constructor(type_ast) 14 | 15 | data_constructors = parse_data_constructors(data_asts) 16 | 17 | %Data{ 18 | type_constructor: type_constructor, 19 | data_constructors: data_constructors 20 | } 21 | end 22 | 23 | @spec parse_type_constructor(Macro.t()) :: TypeConstructor.t() 24 | defp parse_type_constructor({:__aliases__, _, type_name}) do 25 | %TypeConstructor{ 26 | name: type_name, 27 | params: [] 28 | } 29 | end 30 | 31 | defp parse_type_constructor({{:., _, [{:__aliases__, _, type_name}, :t]}, _, type_params_ast}) do 32 | type_params = parse_type_params(type_params_ast) 33 | 34 | %TypeConstructor{ 35 | name: type_name, 36 | params: type_params 37 | } 38 | end 39 | 40 | @spec parse_type_params(Macro.t()) :: [atom()] 41 | defp parse_type_params([]), do: [] 42 | defp parse_type_params(no_parens: true), do: [] 43 | 44 | defp parse_type_params(type_params_ast) do 45 | Enum.map(type_params_ast, fn {type_param_name, _meta, _ctx} -> type_param_name end) 46 | end 47 | 48 | @spec parse_data_constructors(Macro.t()) :: [DataConstructor.t()] 49 | defp parse_data_constructors({:|, _meta, _asts} = ast) do 50 | ast |> or_ast_to_list() |> parse_data_constructors() 51 | end 52 | 53 | defp parse_data_constructors(data_asts) when is_list(data_asts) do 54 | Enum.map(data_asts, &parse_data_constructor/1) 55 | end 56 | 57 | defp parse_data_constructors(data_ast) when not is_list(data_ast) do 58 | [parse_data_constructor(data_ast)] 59 | end 60 | 61 | @spec or_ast_to_list(Macro.t()) :: [Macro.t()] 62 | defp or_ast_to_list({:|, _meta, [h_ast, t_ast]}), do: [h_ast | or_ast_to_list(t_ast)] 63 | defp or_ast_to_list(ast), do: [ast] 64 | 65 | @spec parse_data_constructor(Macro.t()) :: DataConstructor.t() 66 | defp parse_data_constructor({:__aliases__, _, data_name}) do 67 | %DataConstructor{ 68 | name: data_name, 69 | params: [], 70 | record?: false 71 | } 72 | end 73 | 74 | defp parse_data_constructor({{:., _, [{:__aliases__, _, data_name}, :t]}, _, data_params_ast}) do 75 | {data_params, is_record} = parse_data_params(data_params_ast) 76 | 77 | %DataConstructor{ 78 | name: data_name, 79 | params: data_params, 80 | record?: is_record 81 | } 82 | end 83 | 84 | @spec parse_data_params(Macro.t()) :: {[Data.param()], is_record :: boolean()} 85 | defp parse_data_params([data_params_ast]) when is_list(data_params_ast) do 86 | if Keyword.keyword?(data_params_ast) do 87 | params = 88 | Enum.map(data_params_ast, fn {param_name, data_param_ast} -> 89 | {param_name, parse_data_param(data_param_ast)} 90 | end) 91 | 92 | {params, true} 93 | else 94 | raise "expected a keyword list got: #{data_params_ast |> inspect}" 95 | end 96 | end 97 | 98 | defp parse_data_params(data_params_ast) do 99 | params = Enum.map(data_params_ast, &parse_data_param/1) 100 | {params, false} 101 | end 102 | 103 | @spec parse_data_param(Macro.t()) :: Data.param() 104 | defp parse_data_param({name, _, args}) when not is_list(args) do 105 | {:variable, name} 106 | end 107 | 108 | defp parse_data_param(external_type_ast) do 109 | {:external_type, external_type_ast} 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/haex/data/type_constructor.ex: -------------------------------------------------------------------------------- 1 | defmodule Haex.Data.TypeConstructor do 2 | @moduledoc """ 3 | builds type constructor module used to implement a data type 4 | """ 5 | alias __MODULE__, as: T 6 | alias Haex.Data 7 | 8 | @type t() :: %T{ 9 | name: Data.mod_name(), 10 | params: [Data.param_name()] 11 | } 12 | @enforce_keys [:name, :params] 13 | defstruct [:name, :params] 14 | end 15 | -------------------------------------------------------------------------------- /lib/haex/data/type_constructor_builder.ex: -------------------------------------------------------------------------------- 1 | defmodule Haex.Data.TypeConstructorBuilder do 2 | @moduledoc """ 3 | generates AST representation of `Haex.Data.TypeConstructor` to return from 4 | `Haex.data/1` macro 5 | """ 6 | 7 | alias Haex.Ast 8 | alias Haex.Data.DataConstructor 9 | alias Haex.Data.DataConstructorBuilder 10 | alias Haex.Data.TypeConstructor 11 | 12 | @spec build(TypeConstructor.t(), [DataConstructor.t()]) :: Macro.output() 13 | def build(%TypeConstructor{name: name} = tc, data_constructors) do 14 | quote do 15 | defmodule unquote(Ast.mod(name)) do 16 | unquote(type_t(tc, data_constructors)) 17 | 18 | unquote(Enum.map(data_constructors, &DataConstructorBuilder.build/1)) 19 | unquote(Enum.map(data_constructors, &build_helper(tc, &1))) 20 | end 21 | end 22 | end 23 | 24 | @spec build_helper(TypeConstructor.t(), DataConstructor.t()) :: Macro.output() 25 | def build_helper( 26 | %TypeConstructor{} = tc, 27 | %DataConstructor{record?: false, name: name, params: dc_params} = dc 28 | ) do 29 | helper_name = DataConstructor.helper_name(dc) 30 | type_fields = DataConstructorBuilder.type_fields(dc) 31 | args = Macro.generate_arguments(length(dc_params), nil) 32 | helper_type_t = helper_type_t(tc, dc) 33 | helper_when_clause = helper_when_clause(tc, dc) 34 | mod = Ast.mod(name) 35 | 36 | quote do 37 | @spec unquote(helper_name)(unquote_splicing(type_fields)) :: unquote(helper_type_t) 38 | when unquote(helper_when_clause) 39 | def unquote(helper_name)(unquote_splicing(args)), 40 | do: unquote(mod).new(unquote_splicing(args)) 41 | end 42 | end 43 | 44 | def build_helper( 45 | %TypeConstructor{} = tc, 46 | %DataConstructor{record?: true, name: name} = dc 47 | ) do 48 | helper_name = DataConstructor.helper_name(dc) 49 | type_fields = DataConstructorBuilder.type_fields(dc) 50 | type_field_names = type_fields |> Enum.map(fn {name, _field} -> name end) 51 | 52 | type_field_args = 53 | type_fields 54 | |> Enum.map(fn {name, type} -> quote(do: unquote({name, [], Elixir}) :: unquote(type)) end) 55 | 56 | args = Enum.map(type_field_names, fn name -> {name, [], Elixir} end) 57 | helper_type_t = helper_type_t(tc, dc) 58 | helper_when_clause = helper_when_clause(tc, dc) 59 | mod = Ast.mod(name) 60 | 61 | quote do 62 | @spec unquote(helper_name)(unquote_splicing(type_field_args)) :: unquote(helper_type_t) 63 | when unquote(helper_when_clause) 64 | def unquote(helper_name)(unquote_splicing(args)), 65 | do: unquote(mod).new(unquote_splicing(args)) 66 | end 67 | end 68 | 69 | @spec type_t(TypeConstructor.t(), [DataConstructor.t()]) :: Macro.output() 70 | defp type_t(%TypeConstructor{params: params} = tc, data_constructors) do 71 | quoted_params = Enum.map(params, fn param -> {param, [], Elixir} end) 72 | 73 | dc_type_ts = 74 | data_constructors 75 | |> Enum.map(&DataConstructorBuilder.qualified_type_t(tc, &1)) 76 | |> Ast.or_pipe_join() 77 | 78 | quote do 79 | @type t(unquote_splicing(quoted_params)) :: unquote(dc_type_ts) 80 | end 81 | end 82 | 83 | @spec helper_type_t(TypeConstructor.t(), DataConstructor.t()) :: Macro.output() 84 | defp helper_type_t(%TypeConstructor{params: params}, %DataConstructor{} = dc) do 85 | quoted_params = 86 | params 87 | |> Enum.map(&underscore_unused_param(&1, dc)) 88 | |> Enum.map(fn param -> {param, [], Elixir} end) 89 | 90 | quote do 91 | t(unquote_splicing(quoted_params)) 92 | end 93 | end 94 | 95 | defp helper_when_clause(%TypeConstructor{params: params}, %DataConstructor{} = dc) do 96 | Enum.map(params, fn param -> {underscore_unused_param(param, dc), {:var, [], Elixir}} end) 97 | end 98 | 99 | defp underscore_unused_param(param, %DataConstructor{} = dc) do 100 | if DataConstructor.has_variable?(dc, param) do 101 | param 102 | else 103 | :"_#{param}" 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Haex.MixProject do 2 | @moduledoc false 3 | 4 | use Mix.Project 5 | 6 | @vsn "0.0.4" 7 | 8 | def project do 9 | [ 10 | app: :haex, 11 | version: @vsn, 12 | elixir: "~> 1.11", 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps(), 15 | elixirc_paths: elixirc_paths(Mix.env()), 16 | description: 17 | "Haskell as Elixir - A DSL for implementing Haskell style sum and product data types in Elixir", 18 | package: package(), 19 | source_url: "https://github.com/madlep/haex", 20 | homepage_url: "https://github.com/madlep/haex", 21 | docs: [ 22 | main: "readme", 23 | extras: ["README.md"], 24 | source_ref: @vsn 25 | ] 26 | ] 27 | end 28 | 29 | def application do 30 | [ 31 | extra_applications: [:logger] 32 | ] 33 | end 34 | 35 | defp deps do 36 | [ 37 | {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, 38 | {:credo, "~> 1.5", only: [:dev, :test], runtime: false}, 39 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 40 | ] 41 | end 42 | 43 | defp elixirc_paths(:dev), do: ["lib", "examples"] 44 | defp elixirc_paths(:test), do: ["lib", "examples"] 45 | defp elixirc_paths(_), do: ["lib"] 46 | 47 | defp package do 48 | [ 49 | licenses: ["MIT"], 50 | links: %{ 51 | "GitHub" => "https://github.com/madlep/haex", 52 | "Twitter" => "https://twitter.com/madlep" 53 | } 54 | ] 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "credo": {:hex, :credo, "1.5.3", "f345253655f2efe1e4693a03437606462681e91303ebc9e3909c14268effc37a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f7e238c10051cc22515e3f75754200b567d93c00d93be81fc59d47bc3dfdc5be"}, 4 | "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, 6 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 7 | "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, 8 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 9 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 10 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, 12 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 13 | } 14 | -------------------------------------------------------------------------------- /test/data_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DataTest do 2 | @moduledoc false 3 | 4 | use ExUnit.Case 5 | 6 | describe "enum sum data type with constant data constructors" do 7 | # data Color :: Red | Green | Blue | BlueSteel 8 | 9 | test "has type spec defined" do 10 | {:ok, 11 | [ 12 | type: 13 | {:t, 14 | {:type, m, :union, 15 | [ 16 | {:remote_type, m, [{:atom, 0, Color.Red}, {:atom, 0, :t}, []]}, 17 | {:remote_type, m, [{:atom, 0, Color.Green}, {:atom, 0, :t}, []]}, 18 | {:remote_type, m, [{:atom, 0, Color.Blue}, {:atom, 0, :t}, []]}, 19 | {:remote_type, m, [{:atom, 0, Color.BlueSteel}, {:atom, 0, :t}, []]} 20 | ]}, []} 21 | ]} = Code.Typespec.fetch_types(Color) 22 | end 23 | 24 | test "can create data using data module constructors" do 25 | assert Color.Red.new() == Color.Red 26 | assert Color.Green.new() == Color.Green 27 | assert Color.BlueSteel.new() == Color.BlueSteel 28 | end 29 | 30 | test "can create data using type module constructors" do 31 | assert Color.red() == Color.Red 32 | assert Color.green() == Color.Green 33 | assert Color.blue_steel() == Color.BlueSteel 34 | end 35 | end 36 | 37 | describe "sum data type with mix of constant and unnamed constructor parameters" do 38 | # data Maybe a = Nothing | Just a 39 | 40 | test "has sum type defined" do 41 | {:ok, 42 | [ 43 | type: 44 | {:t, 45 | {:type, m, :union, 46 | [ 47 | {:remote_type, m, [{:atom, 0, Maybe.Nothing}, {:atom, 0, :t}, []]}, 48 | {:remote_type, m, [{:atom, 0, Maybe.Just}, {:atom, 0, :t}, [{:var, m, :a}]]} 49 | ]}, [{:var, m, :a}]} 50 | ]} = Code.Typespec.fetch_types(Maybe) 51 | end 52 | 53 | test "can create data using data module constructors" do 54 | assert Maybe.Nothing.new() == Maybe.Nothing 55 | assert Maybe.Just.new("foobar") == {Maybe.Just, "foobar"} 56 | end 57 | 58 | test "can create data using type module constructors" do 59 | assert Maybe.nothing() == Maybe.Nothing 60 | assert Maybe.just("foobar") == {Maybe.Just, "foobar"} 61 | end 62 | end 63 | 64 | describe "sum data type with unnamed constructor parameters" do 65 | # data Either a b = Left a | Right b 66 | 67 | test "has sum type defined" do 68 | {:ok, 69 | [ 70 | type: 71 | {:t, 72 | {:type, m, :union, 73 | [ 74 | {:remote_type, m, [{:atom, 0, Either.Left}, {:atom, 0, :t}, [{:var, m, :a}]]}, 75 | {:remote_type, m, [{:atom, 0, Either.Right}, {:atom, 0, :t}, [{:var, m, :b}]]} 76 | ]}, [{:var, m, :a}, {:var, m, :b}]} 77 | ]} = Code.Typespec.fetch_types(Either) 78 | end 79 | 80 | test "can create data using data module constructors" do 81 | assert Either.Left.new(:fail) == {Either.Left, :fail} 82 | assert Either.Right.new("success") == {Either.Right, "success"} 83 | end 84 | 85 | test "can create data using type module constructors" do 86 | assert Either.left(:fail) == {Either.Left, :fail} 87 | assert Either.right("success") == {Either.Right, "success"} 88 | end 89 | end 90 | 91 | # data Maybe, a, do: Nothing | (Just :: a) 92 | # datacase some_value do 93 | # Nothing -> "Nothing" 94 | # Just :: a -> "Just a" 95 | # end 96 | # 97 | # # data Either a b = Left a | Right b 98 | # data Either, [a, b], do: (Left :: a) | (Right :: b) 99 | # data Either, [a, b] do 100 | # Left :: a 101 | # Right :: b 102 | # end 103 | 104 | # # data Shape = Circle Float Float Float | Rectangle Float Float Float Float 105 | # data Shape, do: (Circle :: [float(), float(), float()]) | (Rectangle :: [float(), float(), float(), float()]) 106 | # data Shape do 107 | # Circle :: [float(), float(), float()] 108 | # Rectangle :: [float(), float(), float()] 109 | # end 110 | 111 | # # data Point = Point Float Float 112 | # data Point, do: [float(), float()] 113 | 114 | # # data Shape = Circle Point Float | Rectangle Point Point 115 | # data Shape, do: (Circle :: [Point.t(), float()]) | (Rectangle :: [Point.t(), Point.t()]) 116 | # data Shape do 117 | # Circle :: [Point.t(), float()] 118 | # Rectangle :: [Point.t(), Point.t()] 119 | # end 120 | 121 | # # data Person = Person String String Int Float String String 122 | # data Person, do: [String.t(), String.t(), integer(), float(), String.t(), String.t()] 123 | 124 | # # data Person = Person { firstName :: String 125 | # # , lastName :: String 126 | # # , age :: Int 127 | # # , height :: Float 128 | # # , phoneNumber :: String 129 | # # , flavor :: String 130 | # # } 131 | # data Person, do: [ 132 | # first_name: String.t(), 133 | # last_name: String.t(), 134 | # age: integer(), 135 | # height: float(), 136 | # phone_number: String.t(), 137 | # flavor: String.t() 138 | # ] 139 | end 140 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------