alpaca-lang / alpaca Goto Github PK
View Code? Open in Web Editor NEWFunctional programming inspired by ML for the Erlang VM
License: Other
Functional programming inspired by ML for the Erlang VM
License: Other
When a function returns a function, it cannot be applied (in MLFE - from Erlang, the resulting function can be called).
module curry_fun
export curried/1
curried arg1 =
let curried2 arg2 =
let curried3 arg3 =
arg1 + arg2 + arg3
in curried3
in
curried2
From Erlang, it works fine:
2> (((curry_fun:curried(1))(2))(3)).
6
But in MLFE, this won't compile: curried 1 2 3
resulting in a compiler error:
mlfe_typer:type_modules/2 crashed with error:{error,
{arity_error,curry_fun,11}}
Using parens (to prevent what I presume is greedy application above) produces a different error:
v = ((curried 1) 2) 3
-------
mlfe_typer:type_modules/2 crashed with error:function_clause
It would be desirable to be able to split code up with several line breaks. This is an example where double line breaks \n\n
are significant and cause a compilation error.
alpaca:compile({text, [<<"module a \n\nf a = let add x = x + x in\n\nadd a a\n\n">>]}).
The following test case fails:
union_type_as_adt_arg_test() ->
Code = "module adt\n\n"
"type union = int | atom\n\n"
"type t = Union union\n\n"
"make () = Union 1",
?assertMatch({ok, _}, module_typ_and_parse(Code)).
Error:
{error, {cannot_unify,adt,7, {adt,"union",[],[]}, t_int}}
It seems like the knowledge about what members are in the union got lost along the way.
This is the line reporting the error: https://github.com/j14159/mlfe/blob/master/src/mlfe_typer.erl#L557
As a user, I'd like to define a type foo/1
as well as a type foo/2
:
type foo 'a = Nothing | Just 'a
type foo 'a 'b = Left 'a | Right 'b
Need these three for lots of things, not least of which is being able to write stuff like basic test matchers without resorting to the FFI, something like
raise_error <term>
raise_exit <term>
(only erlang:exit/1
for now)raise_throw <term>
Maybe it makes sense to introduce special terms instead along the lines of error
, exit
, and throw
, just not sure if those should be reserved words or not.
I'm proposing that these three are parametric error types potentially of the form t_err 'kind 'a
where 'kind
is one of error
, exit
, or throw
. 'a
would be the type of the term used in the various user defined occurrences of t_err
. E.g. raise_error :bad_arith
would type to {t_err, error, t_atom}
.
Unification:
t_err
terms uses unification as normal. Unifying {t_err, throw, t_atom}
with {t_err, error, t_atom}
is a type error as would be {t_err, error, t_atom}
and {t_err, error, t_string}
without a type in scope that unifies them.t_err
terms the errors unify to the other type. E.g. unifying t_int
with {t_err, throw, t_string}
yields t_int
for both types. I think this leaves open the option later to parameterize every type with the potentially raised errors below, somewhat like receivers.The latter of these allows the following to type to 'a -> 'a -> t_bool
:
assert_equal a b = match (a == b) with
true -> true
| false -> raise_throw (not_equal, a, b)
As far as I can tell, we cannot do things like (x == 1) || (y < 4)
and (x == 1) && (y < 4)
.
Mirroring compile:file{1,2}
, there ought to be mlfe:file/{1,2}
. I intend to add this as soon as time permits.
Currently, it is possible to define, but not use, zero-argument (nullary) functions. For example, the following throws a compilation error:
module example
x = 10
run () = x + x
Specifically:
{cannot_unify,main,5,t_int,{t_arrow,[],<0.178.0>}}
In other words, it's failing the type check because x
is compiled as a zero-arg function, whereas the function +
expects only integers. However, this compiles:
module example2
x = 10
run () = x
When called from Erlang, run ()
returns the zero arg function x
, which can be invoked from Erlang to produce the value 10, but as far as I can tell but there is no way of getting the return value of x
within Alpaca.
@j14159 has stated, and I agree, that nullary functions are not desirable and that it would be better if x
is understood in this circumstance as a constant value. We discussed this on IRC and raised several issues:
let _ =
print_string "Hello\n";;
module x
type a = int | b
This doesn't generate an error about an undefined type b
and it should.
Thanks to @danabr for pointing this out in IRC
We might want to look into how edoc extracts specs of functions (which it then includes in the generated documentation).
Consider this example:
module list_tests
is_empty l =
match l with
[] -> true
| _ :: _ -> false
a () = is_empty []
b () = is_empty [:ok]
c () = is_empty [1]
This fails unexpectedly with {error,{cannot_unify,list_tests,12,t_atom,t_int}}
.
The fact that we applied an atom to is_empty, should not specialize the function to only act on atoms.
Need a way to update or add fields to records, e.g.
let r = {x=1, y=2} in
{r | z=3}
It would be great if we could define infix functions, such as the usual suspects (>>=)
, <*>
etc., and even |>
can be implemented this way as a function (such as it is in Ocaml and Elm) instead of hardcoding it in the Elixir manner.
I had a bit of a go at hacking on the parser to allow for infix definitions and had some partial success: master...lepoetemaudit:master but it fell over when parsing any usage of the symbols as a function (it expects 'symbols' which do not include the operators, and this approach was a really inelegant hack to begin with).
I went and looked at the Elm source, which as far as I can tell distinguishes between normal symbols and operator only symbols and realised that to implement this properly we'd probably need to do the same, i.e. remove all the hardcoded infix operators in the lexing phase (and the binary <<
and >>
delimiters are problematic too - perhaps python's b""
style of binary string quoting could be used instead?). Then we could have 'operator' strings defined in the lexer which can go into the environment map as functions, and are recognisable at definition and in usage as infix. It would be fairly easy to either provide the existing math ones as 'built-ins' that wrap the Erlang ones, or switch them out directly as of now in the parser.
I'm willing to give this a go if it is of interest. I'm in awe of the potential of having an ML on the Erlang VM.
Hello!
This keyword seems oddly named as it seems one would use it to call any BEAM language, not just Erlang. Seems odd to use this name to call Elixir or LFE.
Alternatives:
Rather than:
let add x y = x + y in
let square x = x * x in
add 2 (square 3)
I'd like to be able to do something like
let
add x y = x + y;
square x = x * x
in add 2 (square 3)
E.g.
g tuple =
match tuple with
(_, _, x) -> x
The instances of _
aren't being renamed and this should be relatively easy to fix in mlfe_ast_gen.erl
.
As part of AST rewriting before typing occurs the Alpaca compiler renames each variable in order to ensure uniqueness and that nothing escapes from things like receive
s. Arguments to throw
, error
, and exit
aren't being correctly renamed at the moment.
Trying to compile @j14159's example code from another issue:
module guards
type make_it_work = int | string
let f x, is_int x = x + 1
let f x, is_string s = string_append "hello, x"
we get {error,{3,alpaca_parser,["syntax error before: ","','"]}}
For consistency with match
expresssions, guards should be allowed in function heads (just like in Erlang).
Side note: Personally, I would rather drop guards completely from match
expressions, since guards disable exhaustiveness checks ("Having a compiler warn about non-exaustive guards would be impossible in the general case, as it would involve solving the halting problem" (http://stackoverflow.com/a/7109455/347687)), and rather have an if expression.
I've seen in the README.md that there is only “official” support for 18.x because thats what you use @j14159. Personally I have to handle multiple versions on my system and swap back and forth all the time, and as such like to try to maintain compatibility in a given range. I already have an internal project which I have to keep compatible from 16B3 to current, but I do hope beeing able to drop 16 before years end.
Also I am trying to set up travis on my fork and wanted to know which versions of erlang I shall test against.
The current version does compile against the latest minor release of 17, 18 and 19, where 17 fails during compilation phase and 18 and 19 both pass all tests, but fail in dialyzer phase. For 17 there weren't even a dialyzer run, because of known trouble with modules generated from xrl
and yrl
files before OTP 18.
I can reproduce the exact same behaviour on my local system.
From what I have observed on my system and at travis, I'd guess supported range for now should be 18 and 19, while the tests should be run on 18.2, 18.3 and 19.0 at least, dropping 18.2 as soon 19.1 has been released. But I will follow what ever you suggest here.
I will do a WIP-PR in a couple of minutes.
Given:
type option 'a = None | Some 'a
Instead of
map opt f = match opt with
None -> opt
| Some x -> Some (f x)
I want to be able to write
map None _ = None
map (Some x) f = Some (f x)
This will require reworking bits of the parser as terms can nest simple expressions (simple_expr
) in parens making things like pattern matches legitimate terms. We'll need something of a distinction in order to make a list of patterns explicitly that and rule out the occurrence of a match or something equally nonsensical in a function declaration.
The following three tests fail, we should expect that a get_x
not returning an option won't unify with my_map/2
's second argument and thus fail but the typer accepts the integer argument instead of rejecting it. I have not yet confirmed or denied that this behaviour is limited to records.
, fun() ->
Code =
"module fun_pattern_with_adt\n\n"
"type option 'a = None | Some 'a\n\n"
"my_map _ None = None\n\n"
"my_map f Some a = Some (f a)\n\n"
"doubler x = x * x\n\n"
"foo = my_map doubler 2",
?assertMatch(
{error, {cannot_unify, _, _, #adt{}, t_int}},
module_typ_and_parse(Code))
end
, fun() ->
Code =
"module fun_pattern_with_adt\n\n"
"type option 'a = None | Some 'a\n\n"
"my_map _ None = None\n\n"
"my_map f Some a = Some (f a)\n\n"
"doubler x = x * x\n\n"
"get_x {x=x} = x\n\n"
"foo () = "
" let rec = {x=1, y=2} in "
" my_map doubler (get_x rec)",
?assertMatch(
{error, {cannot_unify, _, _, #adt{}, t_int}},
module_typ_and_parse(Code))
end
, fun() ->
Code =
"module fun_pattern_with_adt\n\n"
"type option 'a = None | Some 'a\n\n"
"my_map _ None = None\n\n"
"my_map f Some a = Some (f a)\n\n"
"doubler x = x * x\n\n"
"get_x rec = match rec with {x=x} -> x\n\n"
"foo () = "
" let rec = {x=1, y=2} in "
" my_map doubler (get_x rec)",
?assertMatch(
{error, {cannot_unify, _, _, #adt{}, t_int}},
module_typ_and_parse(Code))
end
Consider the following example:
module shape
type radius = int
type shape = Circle radius
make_circle r = Circle r
test_circle () = make_circle 1
This unexpectedly fails to typecheck with the error:
exception error: no match of right hand side value
{error,{cannot_unify,shape,9,{adt,"radius",[],[]},t_int}}
That is, radius
is considered a distinct type from int
.
The same module in OCaml compiles just fine:
type radius = int
type shape = Circle of radius
let make_circle r = Circle r
let test_circle () = make_circle 1
I think it makes sense for MLFE to behave the same way.
If I would like to make the radius type abstract (or opaque in dialyzer terms), I would hide it in a module (example in OCaml):
module Radius : sig
type radius
val make_radius: int -> radius
end =
struct
type radius = int
let make_radius i = i
end
type shape = Circle of Radius.radius
let make_circle r = Circle r
let test_circle () = make_circle (Radius.make_radius 1)
To achieve the same in MLFE, types should be module local by default, and you would have to export them via a export_type
directive or similar. We would also need to be able to mark types as abstract. In Erlang/Dialyzer this is achieved by using the -opaque
directive.
Taken from the discussion in issue #56 with @danabr and @lepoetemaudit
Single versions of a function will be automatically curried so the following will work:
foo x y = x + y
curry_foo () = 2 |> foo 1 -- results in 3
When there are different versions of the same named function (differing in arity), we will halt typing with an error in any ambiguous case. For example the following would generate an error along the lines of {error, {ambiguous_application, foo/1, foo/2}} - but maybe less hostile than that :)
foo x = x + x
foo x y = x + y
make_an_error () = 1 |> foo
While the following would not:
foo x = x + x
foo x y z = x + y + z
-- unit -> (int -> int)
passes_typing () = 2 |> foo 1
Feedback and differing opinions welcome. I think basic expression application as discussed in #56 has to be addressed before this issue is.
One of the things I'm really enjoying about Elm is the official style guide and automatic code formatting tools. One can write code in any sloppy style, hit save, and then the formatter will rewrite the file in the correct style.
This allows us to avoid extra typing and removes all code style squabbling between developers! Hooray! It'd be great to have this for Alpaca.
I believe this is generally done by converting source code into AST, and then pretty-printing that back. We could leverage the compiler for the parsing/AST generation if there is a public function to do this in the compiler source code.
Cheers,
Louis
This is easily spotted when trying to TAB-complete functions available in an mlfe module:
4> M1.
{compiled_module,basic_adt,"basic_adt.beam",
<<70,79,82,49,0,0,1,176,66,69,65,77,65,116,111,109,0,0,0,
36,0,0,0,6,9,...>>}
5> code:load_binary(element(2, M1), element(3, M1), element(4, M1)).
{module,basic_adt}
6> basic_adt:<tried hitting TAB here>*** ERROR: Shell process terminated! ***
=ERROR REPORT==== 1-Jul-2016::11:41:45 ===
Error in process <0.62.0> with exit value:
{undef,[{basic_adt,module_info,[],[]},
{edlin_expand,expand_function_name,2,
[{file,"edlin_expand.erl"},{line,54}]},
{group,get_line1,4,[{file,"src/4.1.1/group.erl"},{line,568}]},
{group,get_chars_loop,8,[{file,"src/4.1.1/group.erl"},{line,462}]},
{group,io_request,5,[{file,"src/4.1.1/group.erl"},{line,181}]},
{group,server_loop,3,[{file,"src/4.1.1/group.erl"},{line,117}]}]}
Eshell V7.2 (abort with ^G)
1>
M:module_info/1,2
are actually thin shims which call erlang:get_module_info/1,2
which both already work with mlfe modules. I can provide a PR implementing generation of the shims - what do you think?
At the moment we can only access members of records via a pattern match but we need to be able to do
let r = {x=1, y=2} in
r.x + 2
I think this will require rewriting the AST in or before the code generation stage to put a pattern match up front, e.g. in Erlang:
case r of
#{'__struct__' := record, 'x' := R_x} -> R_x + 2
@danabr raised a point about type definitions vs types themselves in PR #116 , e.g.
type opt 'a = Some 'a | None
type int_opt = opt int
The name in the left-hand side of each of these could be viewed in a similar manner as function names in let
bindings, that is, independent of their variables and members/bodies whereas opt int
is a concrete type. Similarly as raised by @ypaq and others elsewhere:
let f x = x + x
could be viewed as syntactic sugar for
let f = fun x -> x + x
or something to that effect.
We might then add an alpaca_type_def
node that binds variables and members to a type name while a member of a type binding stays as an alpaca_type
AST node since it is in fact a concrete type. Functions then might be decomposed into two things as well:
alpaca_fun
node that lists function versions, each of which has their associated variables and bodies.alpaca_fun_def
that binds a name and arity (the latter for convenience) to a single alpaca_fun
AST node.This should make lambdas fairly simple to implement and makes types, functions, and values all operate in a similar manner.
Thoughts?
This fails, the typer tries to unify t_int
and undefined
:
module n
type opt 'a = Some 'a | None
type u = U opt int
let f () = U Some 1
This also fails with the same basic unification error:
module m
export_type t
type t 'a = T 'a
module n
type u 'a = U m.t 'a
let f () = U m.T 1
I haven't dug in too deeply yet but I expect what's happening is that when we do:
type option 'a = Some 'a | None
type something_else = option int
The parser has no idea that int
is supposed to be assigned to a variable 'a
(or any variable, for that matter) and so trying to get that variable from the vars proplist in an #adt{}
yields undefined
.
I think the fix might be pretty simple: when we look through the parameters given for a type that's a member of another type, we just manufacture a new type variable for each type expression that isn't already a type var.
This code parses correctly:
type my_map = map atom atom
This one fails with ["syntax error before: ",[]]
:
type my_atom = atom
type my_map = map my_atom atom
This again parses fine:
type my_atom = atom
type my_map = map (my_atom) atom
i.e, type my_map = map my_atom atom
is parsed as type my_map = map (my_atom atom)
.
The following module should not pass type checking, but it does:
module tree
export height/1, fail/1
type tree 'a = Leaf | Node (tree, 'a, tree)
height t =
match t with
Leaf -> 0
| Node (l, _, r) -> 1 + (max (height l) (height r))
max a b =
match (a > b) with
true -> a
| false -> b
fail () = height 1
We currently have no way to describe a function as a member of an ADT. Something pretty simple to start I think, along the lines of most ML-like things I've seen e.g. type add = int -> int -> int
Failing test illustrating the issue committed in master
on my fork: https://github.com/j14159/alpaca/blob/master/src/alpaca_typer.erl#L4197
When Elixir compiles modules, it prefixes the generated module name with 'Elixir.' (more info here). This means that any modules compiled with Elixir don't risk clashing with Erlang ones, and it keeps the Elixir standard library nicely namespaced. From within Elixir code, you don't need to use the prefix.
I believe the same would be useful in Alpaca - perhaps automatically prefix every generated module with alpaca_
. It means a bit of extra typing when calling from the Erlang side, but it also means we could give nice names to Alpaca standard library modules like string
instead of having to import e.g. alpaca_string
within Alpaca code.
I think scanner.erl
should be renamed because it's very likely to clash with an already existing scanner.beam
. It looks internal, so how about renaming to mlfe_scanner
. Alternatively, and as a way to denote internal modules, maybe we should use mlfe_prv_scanner
and probably rename other internal modules as well.
Thoughts?
I wonder if you plan to make a mailinglist/irc channel or some such to talk about the language. I and a few other people are very interested in discussing features and the like, but issues seem to be the wrong place to do it.
Bridges are to MLFE as ports are to Elm, without the send/receive and subscription semantics.
This is motivated by questions from @imetallica, discussion and feedback from @omarkj, and naming concerns from @lpil.
Example:
bridge append_ints = :erlang :"++" [list int, list int] list_int
Given the above in a module, the compiler will synthesize the function append_ints
, typed to take to integer lists and return one that is a combination of both:
{t_arrow, [{t_list, t_int}, {t_list, t_int}], {t_list, t_int}}
The typer will trust that the author has considered the types involved and will expose this function for type checking. The code generator will create this function in the output Core Erlang AST and programmatically create the necessary checks for the return value. If we follow what Elm has done, this will create some substantial overhead on any recursive type like lists, maps, and recursive ADTs as each element must be checked before returning the result to MLFE code. A more problematic example:
type maybe_io_device = Ok pid unit | Error atom
bridge open_file = :file :open [string, list atom] io_device
If you refer to the erldocs for file:open/2, you'll notice the types I've given above to the bridge are incomplete, for example I'm not accounting for the fd()
type which in the given docs doesn't appear to devolve to a pid. A larger issue is that currently the compiler would render the maybe_io_device
ADT as either {'Ok', Pid}
or {'Error', ErrorAtom}
in any pattern match checking the validity of the return. This is relatively trivial to change and may make sense for simpler handling of common Erlang patterns directly as ADTs with no intermediary translation layer at all.
More specifically, given the changes to how ADTs are rendered, the code above would be synthesized to the following in the code generator:
open_file(Filename, Modes) ->
case file:open(Filename, Modes) of
{ok, IO}=Ok when is_pid(IO) -> Ok;
{error, Reason}=Err when is_atom(Reason) -> Err
end.
This has rather large safety implications:
type try 'x = Success 'x | Error erlang_exception
? I use this type in Scala but does this remove Erlang-ness from the language?unsafe bridge open_file = ...
that doesn't wrap the result?I'm leaning towards point 3 at the moment but curious about other opinions and would like to know if I've missed anything (beyond the complexity checking recursive structures entails).
This issue is for a discussion and collection of ideas at least related to modules and signatures.
In issue #87 the distinction between OCaml's open
and include
came up and how the former exposes the imported module's functions in the module doing the opening. Copied from that issue:
My basic opinion right now: I see value in default implementations of signatures/interfaces but would like to consider more specificity than
open
appears to provide. Thoughts? Links to papers most definitely appreciated :D
I'm excited to play around and hopefully help with mlfe. To start I began the process of creating a rebar3 plugin :) https://github.com/tsloughter/rebar_prv_mlfe
On a side note, I'd suggest either adding rebar.lock
to the .gitignore
of mlfe
or just committing it even though it is empty. Hmm, maybe rebar3 should stop outputting it if it is empty though... I'll think about that :)
Anyway, just letting you know, so feel free to close this issue after reading it.
Hello! This would be ace for us vim users.
You're probably too busy (and an emacs user) to work on this, but I thought I'd note this down for the future :)
In Erlang we can do the following to express two tuple items that are equal:
case Tuple of
{X, X} -> ...
end
In Alpaca we currently have to do this:
match tuple with
(x, y), x == y -> ...
I'd much prefer for us to be able to be like Erlang here:
match tuple with
(x, x) -> ...
The latter seems like a more expressive form to me. This should be relatively simple to solve in the AST generation stage by rewriting multiple occurrences of a symbol in a pattern to a sequence of synthesized names and some added equality guards.
Consider this example:
module user_defined_types
type proplist 'k 'v = list ('k, 'v)
type optlist 'v = proplist atom 'v
This fails with:
{error, {badmatch, {error, {6,mlfe_parser, ["syntax error before: ",["\"atom\""]]}}}
The parser expects a user defined type to only take type variables. See poly_type in mlfe_paser.yrl.
mlfe_typer also has this assumption.
Given
module m
type t = int
The following yields a syntax error but should be allowed:
module n
type u = m.t
This is especially important since the changes discussed in #62 won't permit m.t
to be imported but should still allow it to be referenced.
Short list, driven by discussion on PR #61 with @danabr and from @lepoetemaudit's infix function work:
Per @danabr we should consider a single import
directive that allows importing specific functions, types, or even a subset of a type's constructors.
Ideas/expansions/criticisms most welcome.
Let's collect some suggestions for a name.
Peter Landin -> lander
Robin Milner -> ermil
Tarm (birthplace of Agner Krarup Erlang) plus ML -> tarmel or tarml
ML + erl -> merl, but merl already is a known Erlang module
lambda calculus -> lama
Hi, cool project.
Just an idea: Why not skip support for message send/receive?
Mostly you use the gen_server.erl anyway and if you really want to
send/receive you encapsulate that in your own Erlang module anyway.
Specific items:
alpaca_
__struct__
field to exist.It often is frustrating to update arity for changed functions' signatures. While there are definitely cases when one would keep some functions out from being exported, often times the need to maintain arity in export
becomes a nuisance.
My proposal is to make arity specification in export
optional, and if it isn't specified, export all functions with a given name, regardless of arity.
An empty line between function declarations is necessary to distinguish the two as separate functions but if that empty line contains any whitespace more than a \n
it's not treated as an empty line/break between function definitions.
Consider this function:
duplicate count el =
match count with
0 -> []
| _ -> el :: (duplicate (count-1) el)
Compiling it crashes with:
exception error: no match of right hand side value
{error,{cannot_unify,example,6,
{t_arrow,[<0.90.0>],<0.91.0>},
t_int}}
in function mlfe:compile/2 ([...]/mlfe/src/mlfe.erl, line 55)
However, this type checks and compiles just fine:
duplicate count el =
match count with
0 -> []
| _ ->
let next_count = count - 1 in
el :: (duplicate next_count el)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.