Case class macros for the Crystal Language.
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.

GitHub release Build Status


The case_class macro defines a class whose instances are immutable and provide a natural implementation for the most common methods. It also defines some basic pattern matching functionality, to ease data extraction.


Add this to your application's shard.yml:

    github: lbarasti/case_class


require "case_class"

Let's define a class with read-only fields

case_class Person{name : String, age : Int = 18}

We can now create instances and access fields

p ="Rick", 28) # => "Rick"
p.age # => 28

The equality operator is defined to perform structural comparison

q ="Rick", 28)

p == q # => true

The hash method is defined accordingly. This guarantees predictable behaviour with Set and Hash.

  visitors = Set(Person).new
  visitors << p
  visitors << q

  visitors.size # => 1

to_s is also defined to provide a human readable string representation for a case class instance

puts p # prints "Person(Rick, 28)"

Instances of a case class are immutable. A copy method is provided to build new versions of a given object

p.copy(age: p.age + 1) # => Person(Rick, 29)

Pattern-based parameter extraction

Case classes enable you to extract parameters using some sort of pattern matching. This is powered by a custom definition of the []= operator on the case class itself.

For example, given the case classes

case_class Person{name : String, age : Int = 18}
case_class Address{line1 : String, postcode : String}
case_class Profile{person : Person, address : Address}

and a Profile instance profile

profile ="Alice", 43),"10 Strand", "EC1"))

the following is supported

age, postcode = nil, nil
Profile[Person[_, age], Address[_, postcode]] = profile

age == profile.person.age # => true
postcode == profile.address.postcode # => true

Note that it is necessary for the variables used in the pattern matching to be initialized before they appear in the pattern.

Skipping the initialization step will produce a compilation error as soon as you try to reuse such variables.

Destructuring assignment

Case classes support destructuring assignment. There is no magic involved here: case classes simply implement the indexing operator #[](idx).

person, address = profile

person == profile.person # => true
address == profile.address # => true

The inconvenience with this approach is that the type of both person and address at compile time is going to be String | Int32. This might make your code a bit uglier than it needs to be.

To circumvent this limitation, the to_tuple method is also provided. This assigns the right type to each extracted parameter even at compile-time

profile.to_tuple # => {Person(...), Address(...)}

person, address = profile.to_tuple

person == profile.person # => true
address == profile.address # => true

Case classes and ADTs

If you're into ADTs, then you will enjoy case_class support for inheritance. Here is a sample implementation for a calculator data types.

abstract class Expr(T)

case_class IntExpr{value : Int32} < Expr(Int32)

case_class BoolExpr{value : Bool} < Expr(Bool)

case_class Add{a : Expr(Int32), b : Expr(Int32)} < Expr(Int32)

case_class Eq{a : Expr(Int32), b : Expr(Int32)} < Expr(Bool)

Known Limitations

  • case_class definition must have at least one argument. This is by design. Use class NoArgClass; end instead.
  • trying to inherit from a case class will lead to a compilation error.
case_class A{id : String}
case_class B{id : String, extra : Int32} < A # => won't compile

This is by design. Try defining your case classes so that they inherit from a commmon abstract class instead.

  • case_class definitions are body-free. If you want to define additonal methods on a case class, then just re-open the definition:
case_class YourClass{id : String}

class YourClass
  # additional methods here


To expand the macro

crystal tool expand -c <path/to/>:<line>:<col> <path/to/>


  1. Fork it ( )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request