Algebraic Data Type
Motivation
In my ongoing project on ZX Calculus, I found myself needing an elegant and efficient way to handle objects of specific types and perform operations on their values. Enter Algebraic Data Types (ADTs) - a powerful concept from functional programming. ADTs, coupled with pattern matching, provide a solution that is not only efficient but also expressive, allowing for cleaner, more readable code when dealing with complex data structures.
Algebraic Data Types
Most programming languages provide types to represent primitive data. For
instance, digit numbers are typically represented in Int
, and alphabets in
char
. However, these data types alone might not always convey significant
meaning. It's through their composition that we can represent more complex
concepts. This is where Algebraic Data Types, first introduced in functional
programming, come into play. An Algebraic Data Type (ADT) is a type formulated
by composing other types. The algebra in its name encapsulates the operations
you may execute on these composed types. You can perform addition on types to
create a Sum Type. As an example, in Julia we can define:
@enum Shape Circle=1 Rectangle=2 Square=3 RightTriangle=4
This is a sum type because the total number of possible instances of the ADT
Shape
is the sum of its variants
, i.e., Circle
, Rectangle
, etc. You can
also perform multiplication on types to form a Product Type.
struct Rectangle
length::Int32
width::Int32
end
This is a product type because the total number of potential instances of
Rectangle
is the product of the possible values for length
and width
.
Consequently, you can further compose Sum Types and Product Types to create more
intricate Algebraic Data Types. Here is an example of defining a Product Type
using Expronicon.jl:
using Expronicon.ADT: @adt
@adt public Shape begin
struct Circle
radius::Float32
end
struct Rectangle
length::Int32
width::Int32
end
struct Square
side::Int32
end
struct RightTriangle
side1::Int32
side2::Int32
end
end
Note the usage of the keyword public
; this implies we can use Circle
instead
of Shape.Circle
, making our code more readable and easier to manage.
Pattern Matching
While representing complex concepts through grouped data is beneficial, manipulating this data is equally critical. The first step to handling data is through extraction, which is achieved by destructuring an Algebraic Data Type. This can be generalized into the concept of pattern matching, defined as 'the act of examining a given sequence of tokens for the presence of the constituents of some pattern' [1]. A comprehensive list of pattern matching rules can be found in MlStyle.jl's documentation. For the sake of brevity and relevance, I'll provide examples of pattern matching using the ADT defined with Expronicon and the pattern matching feature provided by MLStyle.jl. This choice was made because the ADT in MLStyle.jl supports generic types, so it won't be type stable. For performance reasons, I'll use the ADT from Expronicon. Both packages interact well.
function enlarge_shape(s::Shape)
@match s begin
Circle(r) => Circle(2r)
Rectangle(l,w) => Rectangle(2l,3w)
Square(s) => Square(s^2)
RightTriangle(a,b) && if => RightTriangle(2a,2*b)
end
end
This code accepts an instance of the Algebraic Data Type Shape
and enlarges
the figure according to the specific type of each passed instance.
Performance Analysis
Roger, author of Expronicon.jl, has made a comparison between using ADT and
naive isa :
ternary operator. It has clear performance gain. For the sake of
time, I will not try to reproduce them here.
Disclaimer
This post has been lovingly crafted using Org Babel, a literal programming support application in Emacs.