I’ve always been fascinated by programming languages.
Every programming language brings its own syntax, semantic and paradigm.
If you have to choose how to implement your own programming language, how would you do it?
Here’s my idea of Inferi, a FLIP (Functional Light Imperative Programming) programming language.
#Index
- Types and Syntax
- Built-in data structures
- Functions! Functions! Functions!
- Impure functions
- Failure handling
- Example program
- Project Status
#Types and Syntax
Inferi is a statically typed programming language. It uses the Hindley Milner type system for type signatures. Let’s define a simple add
function, which adds two numbers:
@spec add : Int -> Int -> Int
add (x y) ->
x + y
Parentheses are completely optional, so developers coming from C-Like languages will find it easier to get started with this language:
@spec add : Int -> Int -> Int
add (x y) -> {
x + y
}
Also, type annotations are completely optional, and can be inferred at compile time (REPL example):
> let add x y -> x + y
> "add : Int -> Int -> Int"
Inferi gives you access to type classes, so that you could refactor the
type signature as follows (just like in Haskell):add
@spec add Num a => a -> a -> a
add (x y) -> {
x + y
}
Functions will always be automatically curried, so that (for instance) the
function can take advantage of partial application:add
@spec add : Int -> Int -> Int
add (x y) -> {
x + y
}
@spec add5 -> Int -> Int
add5 -> add 5
main -> {
print add5(10) -- 15
print add(10 5) -- 15
}
Every Inferi program needs a
function, which will be the entry point of the program itself:main
main -> println "Hello, World!"
#Built-in data structures
Inferi comes with a useful collection of built-in data structures such as:
Maps
data MapExample = {
firstName : String,
lastName : String,
nickname : String
}
@spec mapExample -> Map(MapExample)
mapExample -> {
%{
firstName: "Michele",
lastName: "Riva",
nickname: "Mitch"
}
}
Lists
@spec listExample -> [Int]
listExample -> [10, 50, 20, 1010, 20391, 2039484]
Tuples
@spec tupleExample -> {String, Float, Bool}
tupleExample -> {"I am a string", -30.02, true}
#Functions! Functions! Functions!
Inferi takes a lot from both Haskell and Elixir.
You can pattern match against multiple values by creating different functions, just like in Haskell (incredibly stupid example):
@spec greet : String -> String
greet ("Mitch") -> "Hello, Mitch!"
greet ("Jona") -> "Hello, Jona!"
greet (_) -> "Hello, stranger!"
of course you can bind the passed argument to a variable:
@spec greet : String -> String
greet ("Mitch") -> "Hello, Mitch!"
greet ("Jona") -> "Hello, Jona!"
greet (name) -> "Hello, ${name}!"
You can compose functions by using the pipeline operator:
@spec scream : String -> String
scream (str) -> {
str
|> upcase
|> (\x -> "${x}!!!") -- Lambda function, just like in Haskell
}
or currying just like in Haskell:
@spec scream : String -> String
scream (str) -> str · upcase · (\x -> "${x}!!!")
Also, lambda functions supports the Elixir capture operator for lambda functions:
@spec scream : String -> String
scream (str) -> str · upcase · (& "${&1}!!!")
-- ^ ^ Here we say that we want to use the first argument of our lambda function
-- ^ Here we say that this is a lambda functions
#Impure functions
Unlike Haskell, Inferi is not a purely functional programming language, and it allows us to write some functions that supports impure computations.
Impure function:
import http (call) -- import the "call" function from the "http" library
import list (push)
data RESTPeople = {
name : String `json:"name"` -- a little bit of Golang
age : Int `json:"age"`
}
@spec callRestAPI : Either(Error [String])
callRestAPI? -> {
let mut people : [String]
let mut callResult : [RESTPeople]
&callResult <- await call("https://my-rest-api.com/get-people")
for person in callResult -> {
{ name, age } = person
if age >= 18 {
push(people, name)
}
}
return people
}
We import the
function from the call
library and http
from the push
librarylist
We define a
(in that case, really similar to Golang structs) declaring which value needs to be decoded from the JSON response of our REST API.type
After that, we define a function called
which returns an Either monad (functions with callRestAPI
keywords or other impure computations must return a monad, see Failure handling paragraph).mut
We declare a mutable
variable, which is a list of strings.people
We declare our mutable
variable, which will contain the REST response.callResult
We can finally call our REST API, and we put the result of that call inside the
variable.callResult
After that, we loop over the result (which is a list of
) and we update the previously created list RESTPeople
people
by using the push
function.
As last statement of our function, we need to use the
keywords, which wraps the result into the Either monad.return
If any error occurs, it will wrap the error inside the
result, otherwise it will fill the Left
result of the monad.Right
Pure counterpart:
import http (call?)
data RESTPeople = {
name : String `json:"name"`
age : Int `json:"age"`
}
@spec callRestAPI : Either(Error [String])
callRestAPI? -> {
return $
(await call?("https://my-rest-api.com/get-people") : [RESTPeople])
|> filter(& &1.age >= 18 )
|> map(& drop(&1, "age"))
}
You can also write the exact same function in a more pure manner by using the pipeline operator.
The
operator is used after $
for replacing round brackets, just like in Haskell.return
#Failure handling
Functions could always fail. That’s why Inferi wants to provide three different ways for handling and avoiding runtime errors as follows:
1) Pattern matching:
You can just pattern match over one or more arguments of your functions and avoid or throw errors as you prefer:
divide (x y) -> x / y
divide (x 0) -> error "You can't divide by 0"
main -> {
divide (10 5) -- 2
divide (10 0) -- Runtime error: You can't divide by 0
}
2) Monadic failure handling:
You can add ?
after the function name so that it will encapsulate the function output into an Either
monad:
@spec divide : Either (Num a => a -> a -> a)
divide? (x y) -> x / y
main -> {
divide?(10 5) -- Right (10)
divide?(10 0) -- Left (Error{reason: "Division by 0", args: {10, 5}})
}
If a runtime error occurs, Inferi will let you know by returning a struct containing the reason of the error and the passed arguments (incredibly useful for debug).
You can also implement the monadic failure handling yourself:
@spec divide : Either (Num a => a -> a -> a)
divide? (x y) -> {
try {
result = x / y
Right(result)
} catch error -> {
println (error) -- you can debug the error, send it to Sentry,
-- do whatever you want
Left(Error{reason: "You can't divide by 0!"}) -- The arguments will
-- automatically be attached runtime
}
}
3) Using Status Tuples:
Just like in Elixir/Erlang, you can just return some tuples representing your computation result. This won’t prevent runtime errors, but it’s a best practice for defining different kind of functions:
@spec divide! : Num a => a -> a -> Status(a)
-- ^ it is best practice to set `!` after the function name for telling
-- that you're returning a Status tuple
divide! (x y) (when y == 0) -> {:error, 0, "Division by 0"}
-- ^ Please note this Erlang-style guards
divide! (x y) -> {
result = x / y
{:ok, result, ""}
}
main -> {
print divide(10 5) -- {:ok, 2, ""}
print divide(10 0) -- {:error, 0, "Division by 0"}
}
Status is a private type that represents either
or :ok
atoms (taken from Elixir/Erlang atoms)::error
type Status a -> {Atom, a, String}
type Status a b -> {Atom, a, b}
as you can see in the above definition, we can pass arguments to our types.
That means that we can define different Status
types depending on the function we’re writing.
As an example, this will fail:
@spec divideFloat! : Float -> Float -> Status(Float)
divideFloat! (x y) (when y == 0.0) -> {:error, 0, "Division by 0"}
-- The 0 passed to the Status tuple is of Int type, but the Status Tuple
-- wants a Float (so, 0.0)!
But this will work:
@spec divideFloat! : Float -> Float -> Status(Float)
divideFloat! (x y) (when y == 0.0) -> {:error, 0.0, "Division by 0"}
As you can see in the
definition, we can also customize the third tuple element by explicitly passing its type:Status
@spec doSomethingCool : String -> String -> Status(String Maybe(Error))
doSomethingCool (str1 str2) -> {
returningString = "${str1} ${str2}"
{:ok, returningString, Nothing}
}
main -> {
print doSomethingCool ("Michele" "Riva") -- {:ok, "Michele Riva", Nothing}
}
#Example program
Let’s write a very simple webserver using just the Inferi standard library:
import webserver
import system (env)
{--
Here we get the port number that we want to expose
from our webserver.
--}
@spec port : Int
port -> {
case env("PORT") -> {
| Just(env_port) -> read env_port -- automatic unwrap, is this OCaml?
| Nothing -> 8000
}
}
{--
We then specify a list of tuples which represents
our webserver routes.
--}
@spec routes : [webserver.Route]
routes -> {
[
{"/", indexController}
, {"/about", aboutController}
, {"/greet/:name", greetController}
]
}
{-- Here we write down some controllers --}
@spec indexController : [webserver.Req] -> [webserver.Res]
indexController (_ res) -> {
res.json(%{ -- hey! Is this Express.js?
success: true
})
}
@spec aboutController : [webserver.Req] -> [webserver.Res]
aboutController (_ res) -> {
res.render("about.html") -- oh Express.js, here we meet again
}
@spec greetController : [webserver.Req] -> [webserver.Res]
greetController (req res) -> {
%{ name } = Req.path -- destructure the request path and get the "name" variable
case name -> {
| "" -> res.json(%{success: false, message: "Who should I greet?"})
| name -> res.json(%{success: true, message: "Hello, ${name}!"})
}
}
{-- our main function --}
main -> {
webserver.start(%{
routes -- Hey is this JavaScript?
, port
})
}
#Project Status
The whole Inferi project is just an idea.
No compilers, no specifications, just this article.
Now you may be wondering: “Inferi is really similar to Haskell, Rust, Elixir, OCaml, why are you writing this?”.
Well, I believe that the future of software development is functional.
But even then, purely functional programming will be pretty hard for many people, and we need a language that can be both imperative and functional, taking the best from both worlds.
For that reason, I think that future programming languages will be FLIP (Functional Light Imperative Programming), and Inferi could be my future effort for building such language.