Welcome to the Foopy tutorial!
Foopy is a functional object-oriented programming language, which means it has many of the properties from both FP and OOP.
These include:
- No side-effects
- Everything is an object (except functions, macros and parsers)
- Expressions are memoized
- Functions and classes cannot be redefined
- Objects cannot be mutated
- Expressions are evaluated lazily
Let's begin with the basics.
Foopy's basic types are Integer
, Float
, String
, List
, Tuple
, Bool
and Character
.
List
, Float
, Tuple
and Integer
are the only predefined types in the language.
All of these basic types have shortcuts to instantiate them:
- Integers like
123
- Floats like
17.135
- Characters like
'a'
- Strings like
"aye?"
- Lists like
[1, 2, 3]
- Bools like
true
Strings subclass List, so they act in many ways like lists of Characters.
Characters subclass Integer, so can be treated much like Integers.
You can use Character
to turn an Integer into a Character.
Lists are homogeneous, meaning they are only able to contain one type. They are singly linked lists, where it is easy to take off the front of the list and nothing else. Thusly, performing operations at the end of a list is not very efficient and not recommended.
Note that functions are not a basic type. In fact, they are not types at all; functions are not objects in Foopy, although they are first-class.
Functions are an integral part of Foopy, as with any other language.
As in Haskell, function definition in Foopy looks like tuple x y = (x, y)
.
Foopy also supports pattern-matching such as sumtuple (x, y) = x + y
and first x;xs = x
Foopy supports anonymous functions of the form \xy. x + y
.
Functions are curried in Foopy, meaning the above example is actually equivalent to tuple x = \y. x + y
.
Due to Foopy being partially OOP, most things in the language are objects, which are dissimilar to objects in most languages as they are all immutable entirely.
You can get an attribute from a type with type:attribute
.
You can also use instance:attribute
to get an attribute from an instance of a type.
Getting an attribute that uses an instance variable from an object will raise an exception.
instance.method
is syntactic sugar for instance:method instance
.
To create your own types, use the class
statement.
To begin with, define the signature like class Name x y z
, where Name
is the name of the new type, and x
, y
and z
are the instance variables that the constructor takes.
Afterwards, put a where
on the end of the signature and define the class's methods in an indented block:
class Name x y z where
sum = x + y + z
Note that the class's instance variables, x
, y
and z
can be accessed from everywhere inside the class with only their names, but cannot be accessed with Name:x
.
To make the instance variables public, use:
class Name x y z where
x = x
y = y
z = z
sum = x + y + z
Directly using instance variables in a function is not recommended, as it doesn't allow for Name:sum <Name instance>
, which can at times be quite useful, or Name.sum
. Thusly, our end code becomes:
class Name x y z where
x = x
y = y
z = z
sum us = us:x + us:y + us:z
Macros are a mix between a tool to add new syntax to the language easily and a counterpart to functions that most non-functional programmings have.
IO mode is the way macros and parsers are interpreted.
Instead of just a long chain of different functions being called that eventually culminates in an answer, IO mode is interpreted line-by-line, greedily.
That is, whenever a function is called in IO mode, it is immediately evaluated instead of being evaluated whenever the result is needed.
In addition, IO mode has an operator, <-
, that allows you to bind rebindable variables.
a <- 0
print a # 0
a <- 1
print a # 1
A parser is a special system used in the preprocessing system to be able to have macros. A parser takes two strings and returns an object representing the parsed value.
To create a parser, we first have to start with the signature, parser name before after
where before
represents everything before the macro (explained later), backwards, and after
represents everything after, forwards.
Afterwards, we have an indented block in IO mode to parse the text. Let's write a simple parser that returns everything in front until a space is reached, as a string.
parser nextinfront _ after do # _ is a name that can never be bound to. Whenever you try, the binding is simply ignored. Useful for ignoring arguments that are not needed.
return until (!= ' ') after # until is a function that takes from a list until the filter function returns false, and `(!= ' ')` is equivalent to `\x. x != ' '`
When we use this parser on abc 123 ee e eeee
, it will consume abc
from the code and return abc
. Parsers consume any data that they take from the two lazy strings.
Note that, quite importantly, parsers exist in their own environment. Besides other parsers, parsers don't have any access to outside imported things or defined functions, although you can define functions inside a parser that will be usable outside.
Finally, we get to macros. Macros have a trigger, a parser and a binding. The trigger is what will activate the trigger, the parser is the parser to parse the text around the trigger, and the binding is what the result from the parser will be bound to.
This all looks like macro ! nextinfront infront
.
Then, we have an indented block in IO mode to interpret the parsed data. Finally, we either insert
or return
to make the macro place its result into the code.
macro ! nextinfront infront do
return infront
Now, if we enter !string
, it will become "string"
! If we had used insert infront
instead, it would have literally inserted the code string
and re-parsed the program, which would cause an error in this case but can be quite useful.
Much of the language itself is macros! .
, +
, and !=
are some examples of this.
h