Packages
Fusion DSL is a pluggable domain specific language, Which means packages can be developed to be used in Fusion code.
Every package is simply an elixir module implementing the FusionDsl.Impl
behaviour.
Using packages
To use packages you can add their dependency in mix.exs
and reconfigure :packages
config
parameter for :fusion_dsl
OTP application.
For example imagine we have a package called FusionPackage
and this package is in hex.pm by
name of :fusion_package
. To use this package, first we need to add the dependency.
def deps do
[
{:fusion_package, "~> 0.1.0"} # Copy form package description on hex.pm
...
]
end
Then we will need to add the package in packages list of FusionDSL config.
to do that we will enter change config.exs
of our project to:
use Mix.Config
config :fusion_dsl, packages: [
{FusionPackage, [as: "DifferentName", type: :fusion]}
]
The first element of tuple is the package module we want to use and the second one is the options keyword list.
About the options
:as
: A string to change the name of package in fusion code.:type
: Could be:fusion
or:native
if the module is not a fusion package module.:functions
: For native modules only. Determines list of exported functions for proxy fusion modules.
Create a new package
To create a new FusionDsl package you will need to create a new elixir project.
mix new my_fusion_package
After that you will need to add FusionDSL as a dependency inside your project:
defp deps do
[
{:fusion_dsl, ">= 0.0.0"} # Please use strict versioning instead
...
]
end
Then create a new module and use FusionDsl.Impl
in your module. This module should
hold data about the following:
- What functions does your package provide?
- Implementation of functions.
- Documentation of functions and their arguments.
Basic module
Each FusionDsl Package module should implement at least two functions. The first function
is the __list_fusion_functions__/0
functions. This function returns a list of function names
that this package provide as atoms. e.g. [:foo, :bar]
defmodule FusionPackage do
@moduledoc """
Documentation of the package
"""
use FusionDsl.Impl
@impl true
def __list_fusion_functions__ do
[:foo, :bar]
# These are function names that a fusion developer is allowed
# allowed to call from your package. Implementation of these
# functions should be in this same module.
end
@doc "Documentation for foo and its arguments"
def foo(_ast, env) do
{:ok, "I Am FOO!", env}
end
@doc "Documentation for bar and its arguments"
def bar(_ast, env) do
{:ok, "I Am BAR!", env}
end
end
Here we have a simple module called FusionPackage
which enables us to call foo()
and bar()
functions from our fusion code. Lets dig into it.
List functions
There should be a __list_fusion_functions__/0
function in every fusion package module.
It should return a list of atoms which are function names that this package implements for
fusion. Each function name that you provide here should be implemented in this same module.
Implementing functions
For implementing a function you should create a new function with the same name as you entered
in __list_fusion_functions__
. Like I’ve entered :foo
and :bar
as my function names.
so, I will be implementing two public functions called def foo(...)
and def bar(...)
.
Every fusion package function will have only two arguments. The first argument is the AST (Abstract syntax tree which we will talk about later) of the call which user made and the second one is the environment which the program is running in.
The AST is the syntax tree of a call in fusion. The structure of an ast a 3 element tuple like
this: {FunctionNameAtom, ContextKeywordList, ListOfArguments}
.
- FunctionNameAtom: The atom name of the called function called. (rarely usable for packages)
- ContextKeywordList: Information about the call. like
:ln
which is Line Number of the call in code - ListOfArguments: A List of arguments which user passed to the function. Could be immediate values like
true | false | 1 | 'string' |...
or more ASTs.
So lets say we want foo
to accept a single numeric argument and return the double of
that number.
We will change the implementation of foo
into this.
def foo({:foo, _ctx, [number]}, env) do
result = number * 2
{:ok, result, env}
end
In this function we first got the argument from the AST using pattern matching in function parameters.
Then we will double the number and return it in a tuple like {:ok, ANYTHING_YOU_WANT_TO_RETURN, env}
.
It’s a bit confusing but i promise it will get easy to understand :)
So there are a couple of questions
- Why did we included
env
in arguments and function result? - Why did we returned the result in a tuple?
We included env
because we will need it for:
- Preparing the arguments. User will not always provide immediate values. They sometimes run another
call in order to pass the value to your function. Like
foo(rand(1, 10))
(rand: generate a random number in range) - Writing to assigns. Every fusion package can write their context data in env assigns. You can use
put_assign
andget_assign
to do that. Somehow like how elixir plug works. - Manipulating variables. You can access all variables in the current environment.
- Manipulating procedures. You can change codes live to any procedure BUT the current one.
We returned the result in a tuple just because we needed to return both result
and env
.
Preparing arguments
There is a problem with the foo
implementation. It works in the below situations:
$var = foo(1)
$var = foo(10)
$var = foo(452)
But it wont work in these situations:
$var = foo(rand(1, 2))
$var = foo(SomeModule.generate())
$var = foo(21 + 2)
The reason is, non of these values are immediate values like previous ones. But they are ASTs themselfs.
- The first one is ast of a Kernel Fusion function which generates a random number in provided range.
- The seconds one is another package function which generates a number.
- The third one is a simple calculation.
Why is this happening?
The reason is in fusion, we want the package developer to have full control. So by default, the ASTs of arguments will not be prepared as immediate values.
In order to do that there is a function called prep_arg/2
implemented. We will need to change the foo
implementation to:
def foo({:foo, _ctx, [_] = args}, env) do
{:ok, [number], env} = prep_arg(env, args)
result = number * 2
{:ok, result, env}
end
For more information Visit FusionDsl.Impl
docs.
Existing elixir/erlang modules as Packages
Existing Elixir or Erlang modules can be used as fusion packages without building a separate
fusion module. They can be imported with type: :native
in their opts.
Example
config :fusion_dsl, packages: [
{String, [type: :native]}, # All functions
{Enum, [type: :native, functions: [:sum, :sort]]} # Only sum and sort functions
]
The type: :native
opt, A proxy module will be created during compile time and this module
will implement the FusionDsl.Impl
behaviour.
Then you can use these packages as if they were fusion packages.