Writing Julia-style multiple dispatch code in D
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
README.md
script1.d
script2.d

README.md

Dispatch it like Julia

Writing Julia-style multiple dispatch code in D

August 24, 2017 by Chibisi Chima-Okereke

Introduction

I went to JuliaCon 2017 and learned a lot. Multiple dispatch was one of these things and to my astonishment, it turns out that D is such a flexible language that we can write code in a very similar way to Julia's dispatch style. Multiple dispatch really shines in applications related to mathematical analysis, data science, statistics and machine learning and it is applicable in any other field.

Implementation details

There are three main components to multiple dispatch in Julia, these are:

  • Abstract types
  • Concrete types that inherit from abstract types
  • Methods

Julia's abstract types can be emulated in D using empty interfaces or structs; these basically act as type labels that can inherit from one other abstract type. The main function of abstract types is to serve as a way of dispatching methods. Examples will follow that provide a class-based and a struct-based approach of how to emulate Julia's abstract types, concrete types and methods.

In D, classes (or structs) stand in for Julia's concrete types and they can inherit from one abstract type. Concrete types can only contain data - no functions except a constructor. The main advantage of using D's classes is as a safety feature; should the concrete type ever be accidently dynamically cast a class would accomplish this safely but a struct would not. This said however, we should never allow implicit or indeed runtime casting of types. Any conversions should be explicit for example by specifying a conversion template function convert(To)(From x) rather like the to!T template template function from the std.conv library. Another advantage of using classes is that the syntax of creating type inheritance in D structs is less convenient than for classes.

Methods are represented simply by function overloads created in compile time, no dynamic dispatching ever occurs. In addition methods should be written such that they do not carry out implicit type conversions. Such implicit casting never occurs in Julia, and in fact Julia's methods are compiled when they created making them more like function overloads than method dispatch. One of the interesting aspects of the Julia programming language is that it can be written in a way that is explicitly typed, in fact the best written and most performant Julia libraries tend to be written in this way.

D's UFCS (uniform function call syntax) adds convenient syntactic sugar to overloaded functions. In addition, D's function preconditions are a very useful feature that Julia does not currently have - if you read Julia libraries, you will often see these written as the first-line of the method body.

If you want to know more about how to write multiple dispatch style in Julia, I recommend reading the GLM.jl and Distributions.jl packages.

Example: Density functions for distributions.

This example was influenced by Julia's Distribution.jl package albeit much simplified. It demonstrate how D emulates Julia's multiple dispatch by implementing a density function, and showing how the type hierarchy can be built by representing distributions, discrete and continuous. The implementations of the density functions are straight out of wikipedia's equations for the distributions so do not use them in production!

Class-based approach

The full code for this is given here. The first is to declare the abstract types:

/* The value support of the distribution */
interface ValueSupport{}

/* Discrete and continuous variables */
interface Discrete: ValueSupport{}
interface Continuous: ValueSupport{}

interface Distribution(V: ValueSupport){}

Then we declare the concrete distributions that inherit from these

/* Concrete data-only types that serve the same role as Julia's structs */
class Gamma(T: double): Distribution!Continuous
{
    immutable(T) shape;
    immutable(T) scale;
    this(T shape, T scale)
    {
        this.shape = shape;
        this.scale = scale;
    }
}

class Gaussian(T: double): Distribution!Continuous
{
    immutable(T) mean;
    immutable(T) variance;
    this(T mean, T variance)
    {
        this.mean = mean;
        this.variance = variance;
    }
}


class Uniform(T: double): Distribution!Continuous
{
    immutable(T) a; // minimum
    immutable(T) b; // maximum
    this(T a, T b)
    {
        this.a = a;
        this.b = b;
    }
}


class Exponential(T: double): Distribution!Continuous
{
    immutable(T) lambda;
    this(T lambda)
    {
        this.lambda = lambda;
    }
}


class Poisson(T: double): Distribution!Discrete
{
    immutable(T) lambda;
    this(T lambda)
    {
        this.lambda = lambda;
    }
}

class SomeOtherDistribution: Distribution!Discrete{}

Then a convenience template function to obtain the variable type:

template getVariateType(V)
{
    static if(is (V: Distribution!Continuous))
        alias getVariateType = double;
    else static if(is (V: Distribution!Discrete))
        alias getVariateType = long;
    else static assert(false, "Unknown ValueSupport: " ~ V.stringof);
}

Finally we implement the density functions, note the implementation of the catch all:

/* This is the catch all for unimplemented distributions*/
double density(T, U)(T d, U x)
{
    assert(false, "density function unimplemented for this distribution: " ~ T.stringof);
}

auto density(D: Gamma!T, U = getVariateType!D, T)(D d, U x)
if(isFloatingPoint!U)
in{assert(d.shape > 0 && d.scale > 0, "scale and or shape not > 0, please check parameters");}
body{
    return (1/(gamma(d.shape)*(d.scale^^d.shape)))*(x^^(d.shape - 1))*exp(-(x/d.scale));
}

auto density(D: Gaussian!T, U = getVariateType!D, T)(D d, U x)
if(isFloatingPoint!U)
in{assert(d.variance > 0, "variance of normal distribution is not positive!");}
body{
    return (1/sqrt(2*PI*(d.variance^^2)))*exp(-((x - d.mean)^^2)/(2*d.variance));
}

auto density(D: Uniform!T, U = getVariateType!D, T)(D d, U x)
if(isFloatingPoint!U)
in{assert(d.a < d.b, "a (minimum of the distribution) is not less than b (maximum)!");}
body{
    return 1/(d.b - d.a);
}

auto density(D: Exponential!T, U = getVariateType!D, T)(D d, U x)
if(isFloatingPoint!U)
in{assert(d.lambda > 0, "lambda rate is not greater than zero!");}
body{
    return d.lambda*exp(-d.lambda*x);
}

auto density(D: Poisson!T, U = getVariateType!D, T)(D d, U k)
if(!isFloatingPoint!U)
in{assert(d.lambda > 0, "lambda mean is not greater than zero!");}
body{
    return (d.lambda^^k)*exp(-d.lambda)/gamma(k + 1.0);
}

Now the demonstration ...

void main()
{
    auto GammaDistr = new Gamma!double(3, 2);
    writeln("Gamma(3, 2) @ 4.0: ", GammaDistr.density(4.));
    auto GaussianDistr = new Gaussian!double(0, 1);
    writeln("Gaussian(0, 1) @ 1.96: ", GaussianDistr.density(1.96));
    auto UniformDistr = new Uniform!double(0, 1);
    writeln("Uniform(0, 1) @ 0.5: ", UniformDistr.density(0.5));
    auto ExponentialDistr = new Exponential!double(1.5);
    writeln("Exponential(1.5) @ 2: ", ExponentialDistr.density(2.));
    auto PoissonDistr = new Poisson!double(4);
    writeln("Poisson(4) @ 5: ", PoissonDistr.density(5));
    //auto Other = new SomeOtherDistribution();
    //writeln("SomeOtherDistribution() @ 7: ", Other.density(7)); // throws unimplemented error
}

Struct-based approach

Below is the struct-based approach using D's alias this for inheritance, the full code is given here:

struct ValueSupport{}

struct Discrete{
    ValueSupport Type;
    alias Type this;
}
struct Continuous{
    ValueSupport Type;
    alias Type this;
}

struct Distribution(V: ValueSupport){}

struct Gamma(T: double)
{
    immutable Distribution!Continuous Type;
    alias Type this;
    immutable(T) shape;
    immutable(T) scale;
    this(T shape, T scale)
    {
        this.shape = shape;
        this.scale = scale;
    }
}

Summary

Multiple-dispatch style of programming allows us to write code in a flexible manner, this article shows one of the great features of D - it doesn't impose a particular style of programming and can be used to emulate different programming styles in an explicity and powerful way.