Data types

SCL allows you to define your own types using the data and type keywords.

Simple product types

A product type (analogous to a struct or record in other languages) groups several values together under a single constructor:

data Vector3d = Vector3d Double Double Double

The first Vector3d is the type name; the second is the constructor. They may share the same name. Constructors are functions:

origin :: Vector3d
origin = Vector3d 0.0 0.0 0.0

magnitude :: Vector3d -> Double
magnitude (Vector3d x y z) = sqrt (x*x + y*y + z*z)

Union types (sum types / algebraic data types)

A union type has multiple constructors, each carrying different data:

data StringOrDouble = S String | D Double

describe :: StringOrDouble -> String
describe (S s) = "String: \(s)"
describe (D d) = "Double: \(show d)"

Each constructor can carry a different number and type of fields. Constructors with no fields are enumeration-style:

data Direction = North | South | East | West

data Shape
    = Circle Double              // radius
    | Rectangle Double Double    // width, height
    | Triangle Double Double Double

Record types

Records add named fields to a constructor, enabling field access by name:

data Circle = Circle
    { id     :: String
    , radius :: Double
    , x      :: Double
    , y      :: Double
    }

Fields are accessed with dot notation:

area :: Circle -> Double
area c = pi * c.radius * c.radius

which requires the fields module header feature to work:

module {
    features = [fields]
}

Records can be pattern-matched with named fields (unmentioned fields are ignored):

moveTo :: Double -> Double -> Circle -> Circle
moveTo newX newY Circle{ id = cid, radius = r } =
    Circle { id = cid, radius = r, x = newX, y = newY }

The {..} double-dot notation binds all fields to variables of the same name:

describe :: Circle -> String
describe Circle{..} = "Circle \(id) at (\(x),\(y)) r=\(radius)"

Type aliases

type introduces a new name for an existing type without creating a new constructor. Aliases are purely for readability; the compiler treats them as the same type:

type Point = (Double, Double)
type Name  = String

distance :: Point -> Point -> Double
distance (x1, y1) (x2, y2) =
    let dx = x1 - x2
        dy = y1 - y2
    in sqrt (dx*dx + dy*dy)

Parametric aliases are also supported:

type Pair a = (a, a)
type Vector3 a = (a, a, a)

Deriving Show

To make a custom type printable with show (and displayable in the console), derive the Show instance:

data Color = Red | Green | Blue
deriving instance Show Color

> show Red
"Red"
> show [Red, Green, Blue]
"[Red, Green, Blue]"

deriving instance Show works for simple types. For types with fields, the derived instance shows the constructor name and all field values.

Recursive types

Types can refer to themselves, enabling linked structures:

data Tree a = Leaf | Node a (Tree a) (Tree a)

depth :: Tree a -> Integer
depth Leaf         = 0
depth (Node _ l r) = 1 + max (depth l) (depth r)

Parametric types

Type constructors can take type parameters (like generics in Java):

data Pair a b = Pair a b

swap :: Pair a b -> Pair b a
swap (Pair x y) = Pair y x

The compiler infers concrete types at each call site.