{"id":15056334,"url":"https://github.com/twolodzko/goer","last_synced_at":"2026-02-27T13:38:22.821Z","repository":{"id":233106700,"uuid":"786044691","full_name":"twolodzko/goer","owner":"twolodzko","description":"A minimal Erlang implemented in Go","archived":false,"fork":false,"pushed_at":"2024-04-13T09:58:57.000Z","size":135,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-04-13T23:48:45.481Z","etag":null,"topics":["erlang","golang","interpreter","programming-language"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/twolodzko.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2024-04-13T08:58:32.000Z","updated_at":"2024-04-16T17:34:20.873Z","dependencies_parsed_at":"2024-04-16T17:34:20.310Z","dependency_job_id":"bed5bc0a-ab3c-436b-b903-04ac488840e4","html_url":"https://github.com/twolodzko/goer","commit_stats":null,"previous_names":["twolodzko/goer"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/twolodzko/goer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twolodzko%2Fgoer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twolodzko%2Fgoer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twolodzko%2Fgoer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twolodzko%2Fgoer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/twolodzko","download_url":"https://codeload.github.com/twolodzko/goer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twolodzko%2Fgoer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29897851,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-27T12:09:13.686Z","status":"ssl_error","status_checked_at":"2026-02-27T12:09:13.282Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["erlang","golang","interpreter","programming-language"],"created_at":"2024-09-24T21:49:58.729Z","updated_at":"2026-02-27T13:38:22.793Z","avatar_url":"https://github.com/twolodzko.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `goer`: A minimal Erlang implemented in Go\n\n![The Go's Gopher mascot with the Erlang logo on its belly.](banner.png)\n\n`goer` is a simple language inspired by [Erlang](erlang-book). It implements a subset of Erlang's features with\nsome modifications. I wrote it as a project intended to understand better the concepts behind Erlang, but also\nto compare the differences in the approach to concurrency in Erlang vs Go.\nBoth [Erlang's](erlang-concurrency) and [Go's](go-concurrency) treat it as message passing.\nWhile the two languages are very different, and Go was designed over twenty years after Erlang, you can find many\nparallels in how they treat concurrency. By implementing Erlang in Go, I wanted to have a better feeling of how\nfar-reaching are the similarities. Moreover, in the past, I [implemented Scheme several times], this time, I wanted\nto create a parser for a language that has a syntax. Another inspiration for this was Rob Pike's\n[talk about implementing lexers in Go].\n\n`goer` is a toy language, with a very basic set of functionalities. It doesn't even implement\nany other numerical types than integers. The set of features is minimal by design, whatever can be implemented\nusing `goer`s primitives, would not be implemented as a part of the language itself. For example, strings\nhave very limited support, if you want to manipulate them, use `split` to transform them into lists of character-string\nvalues.\n\n## Features\n\n* The language uses Erlang-like syntax, the syntax coloring for Erlang would mostly work for it out-of-the-box.\n* As in Erlang, everything is immutable.\n* There's extensive support for pattern matching.\n* The language is [tail-call optimized] and looping is done by making recursive function calls.\n* It has basic data types like atoms, booleans, integers, strings, lists, and tuples.\n* It has a REPL.\n\n## Differences from Erlang\n\n* It is a small subset of the Erlang language.\n* It is interpreted, not compiled.\n* Names of some functions (e.g. it has `print`) or the operators (e.g. `!=` instead of `/=`) differ.\n* Erlang has two different syntaxes for defining anonymous and named functions. In `goer` functions are only defined\n  using `fun` keywords (see [below](#Functions)).\n* Erlang relies on linked lists, while `goer` uses [Go slices] instead, this forces using different programming patterns.\n* There are also some small differences, like `_ = _` not raising an error.\n* You wouldn't be able to use it to build a distributed system running on multiple machines, as you would with Erlang.\n\n## Language tour\n\nExpressions that are evaluated need to be finished with `.`, as in Erlang. They can consist of multiple sub-expressions,\nin such a case, they would be separated with `,`.\n\n### Data types\n\n* Atoms are data types defined only by their names. The names are written as words starting with a lowercase letter\n  and can have letters, numbers, `_` and `@` in their names. Examples are `foo`, `other_@Atom`, etc.\n* Booleans: `true` and `false`, are special kinds of atoms. There are basic boolean operations like `not`, `and`,\n  and `or`[^1] that can be applied to booleans.\n* Integers map to Go's `int` type, so are at least 32-bit signed integers. Arithmetic operations `+`, `-`, `*`, `/`\n  (integer division), and `rem` (reminder) can be used with integers. They can also be compared with `\u003c`, `\u003c=`[^2],\n  `\u003e`, `\u003e=`, and the standard `==` and `!=`[^3].\n* Strings are surrounded by double quotes, like `\"Hello, World!\"`. They respect the [same escape characters as Go does],\n  so `\"\\\"Hello,\\nWorld!\\\"\"` is a string that has double quotes and a newline. You can use `str` to convert an arbitrary\n  value to a string (including functions). With `split` string can be converted to a list of single-character strings,\n  this may be used for string manipulation. Strings can be concatenated with the `++` operator.\n* Tuples like `{foo, bar, 1, \"hi\", {}, false}` can contain values of other data types. They can be accessed by\n  [pattern-matching](#pattern-matching) their content.\n* Lists of other values `[1, 2, [foo, {3, 4}], \"bar\"]`. Lists can be concatenated with `++`. They can be cheaply\n  reversed with `rev(Lst)`. Their size can be checked with `len(Lst)`. Their last value can be accessed with `last(Lst)`\n  and list containing all the values but last with `rest(Lst)`. Additionally, `nth(Lst, Idx)` allows for accessing\n  the value at the `Idx` position (zero-indexed) of the list `Lst`.\n\nThere are `is_atom`, `is_bool`, `is_int`, `is_str`, `is_list`, and `is_tuple` functions to check if a value belongs to\na specific type.\n\nFunctions can have lowercase names as well, so `print(\"hi\")` is a function with the `\"hi\"` argument, not an atom.\n\nThe operations in `goer`, like the arithmetic ones, are evaluated left-to-right but follow the standard rules of\nprecedence, and the [precedence] is consistent with Erlang.\n\n### Lists are slightly different\n\nErlang, the same as lisps, and functional languages like OCaml or Haskell, extensively use linked lists. Many of\nthe programming patterns in it evolved from this dependency. [Linked lists] however are in practice less efficient\nthan in theory, and have better modern alternatives. `goer` uses [Go slices] instead, which leads to different\nprogramming patterns.\n\nIn Erlang, the [recommended way](erlang-lists) to add an element to a list is by using the cons operator\n`[Elem | List]` (which has *O(1)* complexity). The syntax is not available in `goer`. Since underneath `goer`'s lists\nare [Go slices], the cheaper way is to append the new element at the back of the slice. For this reason, `goer` rather\nuses the `++` operator, which in Erlang is mostly used to combine lists, e.g. `[1,2] ++ [3,4] == [1,2,3,4]`.\nIn `goer`, to add an element to the list, you would use `List ++ Elem` which adds the element at the back of the list.\nThe `++` operator is overloaded, `[foo] ++ bar` is interpreted the same as `[foo] ++ [bar]`.\n\nFor the same reason as above, head and tail functions are not as useful for dealing with lists as in Erlang.\nInstead, there are `last` and `rest` for accessing the last and everything but the last value.\n\nThis leads to different patterns, for example, to reverse a list in Erlang you [could use a recursive function]:\n\n```erlang\nreverse(Lst) -\u003e reverse(Lst,[]).\nreverse([], Acc) -\u003e Acc;\nreverse([Head|Tail], Acc) -\u003e reverse(Tail, [Head|Acc]).\n```\n\nin `goer` the same function would work in reverse (pun intended):\n\n```erlang\nfun reverse\n    (Lst) -\u003e reverse(Lst, []);\n    ([], Acc) -\u003e Acc;\n    (Lst, Acc) -\u003e reverse(rest(Lst), Acc ++ last(Lst))\nend.\n```\n\n### Everything is immutable\n\nAs with every other language, `goer` has variables. Their names need to start with uppercase letters. The same as with\nErlang, the variables however are [immutable].\n\n```erlang\nX = 1.\nX = 8.    % ERROR: 'X' and '8' do not match\nX = X+1.  % ERROR: 'X' and '2' do not match\n2+5 = Y.  % also works\n```\n\nThis prevents [race conditions] and promotes [*communicating by message passing, not sharing memory*][go-proverb].\n\n### Pattern matching\n\nMany functional languages heavily use pattern matching. The same applies to Erlang and `goer`. As shown above,\nassigning a value to the variable is done using the [\"match\" operator] `=`. `X = 1` means\n\"match `X` against `1`\", if `X` didn't have any value before, it now is equivalent to `1`. The next attempt to match\nit with a different value would fail with an error. Pattern matching can also be used to access the values of tuples:\n\n```erlang\n{First, 2, Third} = {1, 2, 3}.\nFirst = 1.  % ok\nThird = 3.  % ok\n```\n\nThere is also the wildcard character `_` which matches anything. For example, we could match elements of a four-element\nlist to extract a specific one:\n\n```erlang\n[_, _, X, _] = [foo, bar, baz, qux].\nX = baz.  % ok\n```\n\n(The Erlang's `[Head | Tail]` matching is not implemented.)\n\n### Control flow\n\n`goer` has the standard Erlang's `if` statement, which can consist of multiple conditional blocks\n(so it is more like [`cond` in Scheme], than `if` in other languages).\n\n```erlang\nif\n    Cond1 -\u003e\n        Body1;\n    ...;\n    CondN -\u003e\n        BodyN\nend\n```\n\nThe conditions `Cond1` ... `CondN` need to evaluate to booleans. For example,\n\n```erlang\nif\n    X \u003e 5 -\u003e yes;\n    _ -\u003e no\nend\n```\n\nIt also has the `case` statement:\n\n```erlang\ncase Expr of\n    Pattern1 [when Guard1] -\u003e\n        Body1;\n    ...;\n    PatternN [when GuardN] -\u003e\n        BodyN\nend\n```\n\nFor example, [FizzBuzz] written using the case statement would be:\n\n```erlang\nfun fizzbuzz (X) -\u003e\n    case { X rem 3, X rem 5 } of\n        {0, 0} -\u003e fizz_buzz;\n        {0, _} -\u003e fizz;\n        {_, 0} -\u003e buzz;\n        _ -\u003e X\n    end\nend\n```\n\nWe could use the `case` with the guard (after `when`) to check if a value is larger than 5 in an\noverengineered way:\n\n```erlang\ncase 10 of\n    X when X \u003e 5 -\u003e yes;\n    _ -\u003e no\nend\n```\n\nUnlike Erlang, there are no restrictions on guard statements other than that they need to evaluate to booleans.\n\nBoth `if` and `case` would throw an error when no branch matches the conditions.\n\nInstead of Erlang's `try ... catch ... end`, there is a much simpler `try ... recover ... end` statement. It evaluates\nthe expression in the `try` block and in case of an error, and only if, it executes the expression in the\n`recover` block. It is not possible to catch specific errors. Neither of the blocks can be empty.\n\n```erlang\ntry\n    Body1\nrecover\n    Body2\nend\n```\n\n### Functions\n\nErlang has two ways of defining the functions. It allows for either anonymous functions\n\n```erlang\nfun(X) -\u003e X + 10 end\n```\n\nor named functions\n\n```erlang\nfact(0) -\u003e 1;\nfact(N) when N \u003e 0 -\u003e\n    N * fact(N-1).\n```\n\n`goer` combines both syntaxes. The anonymous functions are written the same way, but named functions are written\nlike the anonymous ones, but with the (atom-like) name after the `fun` keyword. This syntax is similar to\n[OCaml's `function`]. The `fact` function from above in `goer` would be\n\n```erlang\nfun fact\n    (0) -\u003e 1;\n    (N) when N \u003e 0 -\u003e\n        N * fact(N-1)\nend\n```\n\nEvery function, build-in or user-defined, needs to return something. Side-effects-only functions are not possible.\nWhen it is not possible to return anything, some functions would throw an error, for example, `last([])` would fail,\nbecause an empty list does not have the last value.\n\n### Concurrency and message passing\n\nAs in Erlang, it is super-easy to start a concurrent process (in `goer`, there's a [goroutine] underneath!) with the\n`spawn` function. In the example below, it would concurrently print \"hi!\", which is a completely pointless thing to do.\nThe `spawn` function returns the process ID (`Pid` below) of the process. The process ID of the current process\ncan be obtained by calling `self()`.\n\n```erlang\nPid = spawn(fun() -\u003e print(\"hi!\") end).\n```\n\nThe process ID can be used to send messages and to communicate with the other processes. The `!` operator means\nsend `Msg` message to the process identified by `Pid`. Sending a message to a process that is not alive anymore\nwould have no effect and no error would be shown as well, the same as in Erlang.\n\n```erlang\nPid ! Msg\n```\n\nThe messages can and need to be received. For this, we use the `receive` block that pattern-matches the messages.\nIf a message that does not match the pattern is received, it is silently dropped. It is possible to specify a timeout\nso that after a specific time the `after` block gets evaluated.\n\n```erlang\nreceive\n    Pattern1 [when Guard1] -\u003e Body1;\n    ...;\n    PatternN [when GuardN] -\u003e BodyN;\nafter\n    Expr -\u003e BodyM\nend\n```\n\nIn Erlang, you could use the `is_process_alive` function to check if the process is still alive. There is [no nice way]\nof checking if a goroutine is alive, so such a function is not implemented. It can be done by implementing the\nprocesses in such a way that they would respond to a healthcheck query, for example:\n\n```erlang\nfun loop()-\u003e\n    receive\n        {Sender, ping} -\u003e\n            Sender ! {self(), pong},\n            loop()\n    end\nend,\n\nPid = spawn(loop),\nPid ! {self(), ping},\nreceive\n    {Pid, pong} -\u003e ok\nafter\n    100 -\u003e timeout\nend.\n```\n\nIt is also [not possible] to kill the goroutine from the \"outside\", so there's no `exit/2` function in `goer` to\ndo this. It could be implemented in a similar way as above, where the process would terminate itself after receiving\nthe \"terminate\" message. It is similar to the `link` function, which makes the linked processes in Erlang [die together].\nLinking could be implemented in `goer` by the linked processes keeping the list of linked processes and on exit\n(you could use `try ... recover ... end` here) they would send the \"terminate\" signal to the other processes.\n\n## Grammar\n\n`goer`'s grammar in [EBNF] form is:\n\n```c\nDummy           = '_'\nAtom            = ( 'a'..'z' ) [ 'a'..'z' | 'A'..'Z' | '0'..'9' | '_' | '@' ]*\nVariable        = ( 'A'..'Z' ) [ 'a'..'z' | 'A'..'Z' | '0'..'9' | '_' | '@' ]*\nBool            = 'true' | 'false'\nInt             = ( '0'..'9' )*\nString          = '\"' ( Any - '\\\"' )* '\"'\nTuple           = '{' Exprs '}'\nList            = '[' Exprs ']'\nBracket         = '(' Expr ')'\nTerm            = Atom | Bool | Int | String | Tuple | List\nExpr            = Term | Variable | Dummy | Bracket | UnaryOperation | BinaryOperation | Call | Fun\nExprs           = Expr [ ',' Expr ]*\nBlock           = Exprs '.'\nOp              = '+' | '-' | '*' | '/' | 'rem' | '==' | '!=' | '\u003c' | '\u003c=' | '\u003e' | '\u003e=' | '=' | '!' | 'and' | 'or'\nUnaryOperation  = ( '+' | '-' | 'not' ) Expr\nBinaryOperation = Expr Op Expr\nCall            = ( Atom | Variable | Bracket ) '(' Exprs ')'\nGuard           = 'where' Exprs\nFunBranch       = '(' Exprs ')' [ Guard ] '-\u003e' Exprs\nFun             = 'fun' [ Atom ] FunBranch [ ';' FunBranch ]* 'end'\nIfBranch        = Expr '-\u003e' Exprs\nIf              = 'if' IfBranch [ ';' IfBranch ]* 'end'\nCondBranch      = Expr [ Guard ] '-\u003e' Exprs\nCase            = 'case' Expr 'of' CondBranch [ ';' CondBranch ]* 'end'\nReceive         = 'receive' CondBranch [ ';' CondBranch ]* 'after' IfBranch 'end'\nTry             = 'try' Exprs 'recover' Exprs 'end'\n```\n\n [^1]: They are short-circuited operators like `andalso` and `orelse` in Erlang.\n [^2]: Erlang uses `=\u003c`, but `\u003c=` seems to be more prevalent in other languages.\n [^3]: Erlang uses `/=`, similar, `!=` seems to be more common.\n\n [erlang-book]: https://learnyousomeerlang.com/contents\n [Linked lists]: https://www.youtube.com/watch?v=YQs6IC-vgmo\n [erlang-lists]: https://learnyousomeerlang.com/starting-out-for-real#lists\n [Go slices]: https://go.dev/blog/slices-intro\n [talk about implementing lexers in Go]: https://www.youtube.com/watch?v=HxaD_trXwRE\n [EBNF]: https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form\n [processes]: https://www.erlang.org/doc/reference_manual/processes\n [go-proverb]: https://www.youtube.com/watch?v=PAAkCSZUG1c\u0026t=168s\n [goroutine]: https://www.youtube.com/watch?v=oV9rvDllKEg\n [go-pipelines]: https://tip.golang.org/blog/pipelines\n [go-share-memory]: https://go.dev/blog/codelab-share\n [erlang-concurrency]: https://learnyousomeerlang.com/the-hitchhikers-guide-to-concurrency\n [implemented scheme several times]: https://twolodzko.github.io/posts/implementing-lisps.html\n [no good way]: https://stackoverflow.com/questions/16105325/how-to-check-a-channel-is-closed-or-not-without-reading-it\n [could use a recursive function]: https://stackoverflow.com/a/22300045/3986320\n [immutable]: https://twolodzko.github.io/posts/hot-potato.html\n [race conditions]: https://stackoverflow.com/q/34510/3986320\n [\"match\" operator]: https://www.erlang.org/doc/reference_manual/patterns\n [FizzBuzz]: https://rosettacode.org/wiki/FizzBuzz\n [no nice way]: https://stackoverflow.com/q/16105325/3986320\n [not possible]: https://stackoverflow.com/q/42560109/3986320\n [die together]: https://learnyousomeerlang.com/errors-and-processes\n [precedence]: https://www.erlang.org/doc/reference_manual/expressions#operator-precedence\n [`cond` in Scheme]: https://conservatory.scheme.org/schemers/Documents/Standards/R5RS/HTML/r5rs-Z-H-7.html#%_sec_4.2.1\n [tail-call optimized]: https://github.com/kanaka/mal/blob/master/process/guide.md#step-5-tail-call-optimization\n [same escape characters as Go does]: https://pkg.go.dev/strconv#Unquote\n [OCaml's `function`]: https://dev.realworldocaml.org/variables-and-functions.html#declaring-functions-with-function\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftwolodzko%2Fgoer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftwolodzko%2Fgoer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftwolodzko%2Fgoer/lists"}