-
Notifications
You must be signed in to change notification settings - Fork 2
a pure lazy functional programming language to make ASCII art animations (and other things too)
License
olekawaii/thorn
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
thorn is pure lazy functional programming language for making ascii art animations
check out some examples in the examples/ directory.
There are currently no generics or type classes so it's a pain to use, but
they'll be relatively easy to implement in the future.
It's still in active development. If you encounter any bugs or issues, please
let me know.
INSTALLATION
====================
thorn has no dependencies. You can build it with
$ cargo build --release
The executable is at target/release/thorn. Move it to somewhere in your $PATH
The programs to generate shell scripts and images are found in the converters/
directory. You can build those with ghc. Once you get those installed, there is
also a useful shell script 'uwu' in the scripts/ directory to make building
your projects quick and easy. You need ffmpeg for generating gifs with uwu. All
of these are optional.
The thorn compiler works like a calculator. It parses and compiles the source
code into an internal representation, but afterwards also executes it, printing
the evaluated main function.
$ thorn main.th | thorn-to-shell | sh
to convert a thorn program to a posix shell animation and play it.
Expression Syntax
====================
An expression is either a lambda, match, or function with arguments (a tree).
match expression
--------------------
All branches must have the same indentation. The mandetory keywords are
'match', 'with', and 'to'.
.----------------------------------------------------------.
| match <expression> with |
| <pattern> to <expression> |
| <pattern> to <expression> |
| ... |
'----------------------------------------------------------'
lambda expression
--------------------
.----------------------------------------------------------.
| lambda <pattern> <expression> |
'----------------------------------------------------------'
argument tree
--------------------
1 .----------------------------------------------------------.
| root word word ... <expression>? |
'----------------------------------------------------------'
sum(v, product(iii, x)) (in C syntax) as an argument tree would be
.----------------------------------------------------------.
| sum v product iii x |
'----------------------------------------------------------'
This is possible because the types of all variables are known at all times,
letting us use polish notation to group them into expressions
2 .----------------------------------------------------------.
| root |
| <expression> |
| <expression> |
| ... |
'----------------------------------------------------------'
With the second syntax every argument of the root must be on its own line
with the correct indentation. This the only way to use a lambda or match
expresion directly as an argument to a function.
ALGEBRAIC TYPES
====================
.----------------------------------------------------------.
| type bool contains |
| true |
| false |
| |
| type list_of_bools contains |
| empty_list |
| prepend bool list_of_bools |
| |
| type maybe_bool contains |
| none |
| some bool |
| |
| define head of_type fn list_of_bools maybe_bool as |
| lambda input_list match input_list with |
| empty_list to none |
| prepend head tail to some head |
'----------------------------------------------------------'
.----------------------------------------------------------.
| define main of_type maybe_bool as |
| head prepend true prepend false empty_list |
'----------------------------------------------------------'
output: some true
.----------------------------------------------------------.
| define main of_type maybe_bool as |
| head empty_list |
'----------------------------------------------------------'
output: none
TYPE ERRORS
====================
One of the main goals of this language is to have good (and I mean VERY good)
errors. Consider this example that has multiple type errors:
.----------------------------------------------------------.
| define or of_type fn bool fn bool bool as ... |
| |
| define sum of_type fn int fn int int as ... |
| |
| define true of_type bool as ... |
| |
| define five of_type int as ... |
| |
| define main of_type int as sum five or true five | <-
'----------------------------------------------------------'
There are who issues with this code:
1. `or` expects two bool arguments but the second argument is an int
2. `sum` expects two ints but the second argument is whatever the
output of `or` would be (a bool)
In all programming languages that I know of, you get the most nested errror
first (1) and after it's resolved you get the less nested one (2). The error
C would give would be something like:
compilation error
|
10 | sum(five, or(true, five)
| ~~~~ mismatched types
second argument of `or` expected a `bool` but found an `int`
However, in my implementation, you actually get the least nested error first.
compilation error
|
10 | sum five or true five
| ~~ mismatched types
`sum five` expected an `int` but `or` could never evaluate to it
It is a more logical approach. The bigger error in this program isn't that
the argumunts to `or` are incorrect, it's that the whole `or` expression
shouldn't even be there.
This also makes it so all errors are found from left to right. if you get an error
at the end of the line, you know that everything that came before it was
correct. This also eliminates the painful type errors you get in haskell when
you misplace paranthesis.
DESTRUCTURING
====================
Consider the following function that finds the distance between
two points. Ignore the functions that we haven't defined.
.----------------------------------------------------------.
| type point contains |
| point int int |
| |
| define distance of_type fn point fn point float as |
| lambda first_point lambda second_point |
| match first_point with |
| point xa ya to match second_point with |
| point xb yb to sqrt sum |
| squared diff xb xa |
| squared diff yb ya |
'----------------------------------------------------------'
This is very verbose because we're patternmatching on all the points to get
their x and y values. However, because the point type only contains one
variant, we can actually patternmatch directly in the lambda expression
like so
.----------------------------------------------------------.
| define distance of_type fn point fn point float as |
| lambda point xa ya lambda point xb yb sqrt sum |
| squared diff xb xa |
| squared diff yb ya |
'----------------------------------------------------------'
In fact, lambda, match, and (in the future) let expressions share the same
syntax and semantics for capturing variables.
RECURSION
====================
There are no loops. The only way to 'loop' is to use recursion.
.----------------------------------------------------------.
| define main of_type nat as |
| sum '
| three .----------------------.
| succ one | one = 1 |
| | succ one = 2 |
| type nat contains | succ (succ one) = 3 |
| one | and so on |
| succ nat '----------------------'
| .
| define three of_type nat as succ succ one |
| |
| define sum of_type fn nat fn nat nat as |
| lambda x match x with |
| one to succ | <---.
| succ a to lambda y succ sum a y -- recursive | |
'----------------------------------------------------------' |
output: succ succ succ succ one |
By the way, if you look at the third to last line, the `one` branch returns a
`succ` while the 'succ pred_x' branch returs a lambda expression. This is
because the `succ` data constructor is a function. (succ one) has the type (nat)
but (succ) has the type (fn nat nat). All data constructors are curried.
Alternatively we could've rewritten `succ` as `lambda x succ x`, which would
have the same meaning.
LAZY EVALUATION
====================
The whole language is lazily evaluated. The only thing evaluated strictly
is the main function.
Here are some examples. Expressions are only evaluated when patternmatched on.
If you don't try to match on something, it will never be evaluated.
To evaluate an expression, it is rewritten until it starts with a
data constructor.
.----------------------------------------------------------.
| type tuple_of_nats contains |
| tuple nat nat |
| |
| define is_equal of_type fn nat fn nat bool as |
| lambda x lambda y match tuple x y with |
| tuple one one to true |
| tuple succ a succ b to is_equal a b |
| _ to false |
'----------------------------------------------------------'
.----------------------------------------------------------.
| define main of_type bool as |
| is_equal |
| succ succ one |
| succ succ one |
'----------------------------------------------------------'
output: true
.----------------------------------------------------------.
| define main of_type bool as |
| is_equal |
| succ succ one |
| succ succ succ one |
'----------------------------------------------------------'
output: false
.----------------------------------------------------------.
| define main of_type bool as |
| is_equal |
| succ succ succ one |
| infinity |
| |
| define infinity of_type nat as succ infinity |
'----------------------------------------------------------'
output: false
.----------------------------------------------------------.
| define main of_type bool as |
| is_equal infinity infinity |
'----------------------------------------------------------'
output: *hangs at runtime*
.---------------.
.------------------------------------------- | ... is syntax |
| define main of_type bool as | for undefined |
| is_equal '---------------'
| succ succ one .
| succ succ succ succ ... |
'----------------------------------------------------------'
output: false
.----------------------------------------------------------.
| define main of_type bool as |
| is_equal |
| succ succ succ one |
| succ succ ... |
'----------------------------------------------------------'
output: *panics at runtime*
UNDEFINED
====================
Undefined expressions panic at runtime if you attempt to evaluate them.
They are useful for unimplemented or unreachable code
.----------------------------------------------------------.
| define main of_type video as ... |
'----------------------------------------------------------'
ART
====================
This code defines an animation of a swinging tail. The animation is 6x4 in
size and has 2 frames.
.----------------------------------------------------------.
| define dragon_tail of_type video as art vi iv |
| \ ).6|||6( / 6|||6. |
| ) / ..6|6. \ ( .6|6.. |
| ( / .6|6.. \ ) ..6|6. |
| V ..6... V ...6.. |
'----------------------------------------------------------'
The art macro is expanded into regular code during the tokenization phase.
Every frame is made up of who rectangles, one with the ascii art and the
other with the corresponding colors. Colors 0..7 correspond to ANSI colors
black..white, '.' marks transparency, and '|' marks a space.
There is also special syntax for including local/global variables in the
art.
To use a color variable you just replace the color number with the variable
name like so:
.----------------------------------------------------------.
| define dragon_tail of_type fn color video as |
| lambda c art vi iv |
| \ ).c|||c( / c|||c. |
| ) / ..c|c. \ ( .c|c.. |
| ( / .c|c.. \ ) ..c|c. |
| V ..c... V ...c.. |
'----------------------------------------------------------'
Now instead of the tail being hardcoded cyan, the calling function decides
its color.
To include a variable character, set the color to '$' and use the variable
name in the art section.
you can have a custom tail tip for example:
.----------------------------------------------------------.
| define dragon_tail of_type fn character video as |
| lambda t art vi iv |
| \ ).6|||6( / 6|||6. |
| ) / ..6|6. \ ( .6|6.. |
| ( / .6|6.. \ ) ..6|6. |
| t ..$... t ...$.. |
'----------------------------------------------------------'
note that the character type contains the color as well as the character. Here
is the implementation in core.th
.----------------------------------------------------------.
| type character contains |
| space |
| char visible_character color |
'----------------------------------------------------------'
You can see how the video type is implemented in the core.th and there are
many useful functions in video.th
OUTPUT
====================
After compiling/running the program, the output is the fully-evaluated main
function (a tree of data constructors). In fact, if you plug the output into
the main function, you will get the same output. To turn this output into
something you actually want, you have to interpret it with an external
program (like thorn-to-shell for ascii art animations for example).
About
a pure lazy functional programming language to make ASCII art animations (and other things too)
Topics
Resources
License
Stars
Watchers
Forks
Releases
No releases published
Packages 0
No packages published