Fastfwd
Plugin-style function forwarding in Elixir, for adapters, factories and other fun. Fastfwd can be used to provide functionality similar to Rails' ActiveRecord type column, or to allow third party libraries or applications to extend the functionality of your code.
Installation
The package can be installed by adding fastfwd to your list of
dependencies in mix.exs:
def deps do
[
{:fastfwd, "~> 0.2.0"}
]
endPurpose
Elixir lacks OOP inheritance and polymorphism but different types of data can be processed by selecting a different modules for each type.
Fastfwd proves an alternative to hardcoding which module should be used for each type of data. You might be using a case statement, like this:
case bread.type do
:barm -> Bread.Barm.bake(bread.quantity)
:stottie -> Bread.Stottie.bake(bread.quantity)
:sliced -> Bread.SlicedWhiteLoaf.bake(bread.quantity)
:sourdough -> Bread.Sourdough.bake(bread.quantity)
endThis has some disadvantages: whenever you add a new module to handle a new data type you need to update all the case statements, and you need to know all the modules in advance - it isn't possible to automatically extend your application with libraries containing extra data processing modules.
Fastfwd provides an alternative approach: it will search your application and libraries to find suitable "receiver" modules, build a table of suitable modules, and forward calls from a frontend "sender" module to the appropriate receiver.
Instead of using a case statement call the method on the sender module like this:
Bread.bake(bread.type, bread.quantity)If caching is enabled then Fastfwd can be quite fast - not as fast as using a static case statement, but close. In my benchmarking it takes 7µs vs 6µs for an equivalent case statement.
Usage
1) Using the Sender and Receiver modules
The easiest way to use Fastfwd is to use the included Fastfwd.Sender and Fastfwd.Receiver modules to extend your own modules. If you
want more control then the Fastfwd module provides some utility functions that you can use to extend or replace
the bundled Fastfwd.Sender and Fastfwd.Receiver.
Read the Fastfwd.Sender and Fastfwd.Receiver docs for more information.
2) Writing your own Sender and Receiver modules
When you use the Fastfwd.Sender it will by default search for modules
that implement the Fastfwd.Behaviours.Receiver behaviour, like Fastfwd.Sender. If
you need different behaviour you can write your own implementations of
Fastfwd.Behaviours.Receiver or Fastfwd.Behaviours.Sender behaviour or
specify different behaviours.
Read the Fastfwd.Behaviours.Sender and Fastfwd.Behaviours.Receiver docs for more information.
3) Using the utility functions
Fastfwd works by building a list of suitable modules, then collecting the tags of those modules,
then providing an alternative to Kernel.apply that uses a tag to select a module. The supplied
Fastfwd.Sender makes this process faster by caching the module list and tag lookup table using
Discord's FastGlobal library.
The top level Fastfwd module contains some useful functions for listing modules and
extracting tags, while Fastfwd.Modules and Fastfwd.Module have some lower-level
utilities for filtering lists of modules.
These utilities can be used to validating or process tags before using them to call functions, such as when read and writing to a database or accepting user input.
Examples
Using the Sender and Receiver modules
In this example we create modules to bake various type of bread. We will create a Bread module that receives order structs and passes the request to the correct sub-module.
The modules doing the hard work use the Fastfwd.Receiver module.
defmodule Bread.Barm do
use Fastfwd.Receiver, tags: [:barm]
def bake(loaves), do: "Baking #{loaves} Lancashire barm cakes"
end
defmodule Bread.Stottie do
use Fastfwd.Receiver, tags: [:stottie]
def bake(loaves), do: "Baking #{loaves} Newcastle stotties"
end
defmodule Bread.SlicedWhiteLoaf do
use Fastfwd.Receiver, tags: [:sliced]
def bake(loaves), do: "Baking #{loaves} sliced white loaves"
end
defmodule Bread.Sourdough do
use Fastfwd.Receiver, tags: [:sourdough, :paindecampagne]
def bake(loaves), do: "Baking #{loaves} pain de campagne"
end
Another module is configured to act as a forwarder - this is the module
the rest of your code will interact with. It uses the Fastfwd.Sender
module, and is configured to search for suitable modules under the
'Bread' module namespace.
defmodule Bread do
use Fastfwd.Sender, namespace: Bread, cache: true
def bake(type, loaves) do
fwd(type, :bake, [loaves])
end
end
We can then call the appropriate bake function via the forwarder, and the correct sender module's bake function will be called.
Bread.bake(:stottie, 8)Integrating With Ecto
One of the biggest reasons Fastfwd was created was to mimic the way
Ruby on Rails' ActiveRecord will return polymorphic sub-classes using a
type field in the database.
To only store records with a type that matches available tags, verify that the record's tag is actually supported by a module:
def changeset(bread_order, params \\ %{}) do
user
|> validate_inclusion(:type, Fastfwd.Tags.to_strings(Bread))
|> cast(params, [:type, :quantity])
|> validate_required([:type, :quantity])
endAPI Documentation
Full API documentation can be found at https://hexdocs.pm/fastfwd.
Contributing
You can request new features by creating an issue, or submit a pull request with your contribution.
Copyright and License
Copyright (c) 2019 Digital Identity Ltd, UK
Fastfwd is MIT licensed.
References
- https://elixir-lang.org/getting-started/typespecs-and-behaviours.html#dynamic-dispatch
- https://en.wikipedia.org/wiki/Dynamic_dispatch#Smalltalk_implementation
- https://en.wikipedia.org/wiki/Forwarding_(object-oriented_programming)#Applications
- http://charlesleifer.com/blog/django-patterns-pluggable-backends/
Thanks
The "fast" in Fastfwd is from Discord's FastGlobal library
The "fwd" in Fastfwd is based on the technique in this blog post