Crystal heavily utilises the famously clean & pragmatic syntax of Ruby, extended with a strict static type system, simplifying & improving the experience of OOP in the ruby paradigm.[1]
Much like rust, it has a fair amount of type safety built-in; the common case of the billion dollar mistake is simply not possible within the language due to null reference checking embedded into the compiler, as all types can be checked for being nil-able.
Everything in crystal is an object. Base types, classes, functions, everything.
This is why we can have strong type casting such as:
my_num =Int32.new(256)
pp my_num # => 256: Int32
# Which is also equivalent to simply:
num : Int32 =256
# In crystal, the space after the declaration is required # otherwise it will be inferred as a property selector.
Similarly, we can write the same space within memory; i.e:
string =IO::Memory.new "hihi!"# In crystal, calling functions with parens is optional.
io.pos # => 0
io.gets 2# => "hi"
We can extend this logic into more high-level components by creating our own classes, in crystal known as abstract classes.
These are classes that essentially define the structure & data that all subsequent classes must have.
In a real-world context, this may be useful when instantiating several types of the same object. However, it is not recommended to use abstract classes as base blueprints for other objects/classes, which will be covered a bit later.
A good example of using abstract data types is shown here;
abstractclassEnginedefinitialize(@name:String,@version:String)
property memthread : Fiber
end
abstractdefrun
abstractdefstop end
classRustEngine: Engine definitialize(@name=“CTEngine”,@version=“0.1.0”)
memthread =Fiber.new end
defrun
spawn name:@namedo
memthread.timeout 12.seconds end end
defstop # Halt the engine execution by stopping a fiber.
memthread.yield end end
Encapsulation is something that should always be avoided in Object-Oriented Programming, unless you really know what you are doing. The most common nightmare it can cause is a hierarchy of dependencies, cross-cutting to share values & extend from other classes & in turn, shared mutable state.
Which, as every Java programmer knows, is a special layer of hell.
Say for example you are using encapsulation techniques to write an inventory for a pet store, and you wish to archive all the animals within the store, with optimised searching for an ORM. With encapsulation, one’s first thought would be to make a base class for Animal, as this helps with ORM pattern searching.
abstractclassAnimal
property legs, arms, nose
end
classCat: Animal
property meow # Defining the properties of a cat end
classDog: Animal
property bark end
So far so good. However, the main issue occurs when there is an animal that shares the properties of both
types of animal; for example, an animal that could have the qualities of both a cat and dog; and be listed as either a cat or a dog would require having to first go through the tree to derive from both animal, then cat, then dog, then whatever else was before, which makes objects extremely hard to debug and maintain long-term longevity.
Finally, crystal’s intuitive macro system works extremely well for polmorphic & dynamically generated code during runtime.
For example, in the use of creating a html template:
macro iteration(name, max_limit)def{{name}}
iter =0
breakpoint = iter / rand({{max_limit}})
loop do
iter +=1breakif iter =={{max_limit}}breakif iter == breakpoint
endreturn iter
endend
Crystal also has a modern package management system, known as shards, which are modules, binaries & libraries for the language.[2]
To set up a crystal project, the easiest way to do this is also with shards, by running: shards init --app, or with --lib to generate a library package with crystal’s in-built test suite.
This suite can be accessed & used by writing test cases within the spec folder of your newly generated project.