# Are unions in Crystal considered as tagged unions (sum type)?

or are they untagged unions? I’m mainly trying to relate to principles in other programming languages. I couldn’t find anything in the docs. Thanks.

I’m not sure what your question is aiming at, why is this relevant to you?

For some simple code like

``````struct A
@a : Int64  = 1
@b : Int64  = 2
end

a = "foo".as(String|Int32)
b = "foo".as(String|Int64)
c = "foo".as(String?)
d =  1.as(Int32?)
e = "foo".as(String|A)
``````

The types in the LLVM IR are:

``````%"(Int32 | String)" = type { i32, [1 x i64] }
%"(Int64 | String)" = type { i32, [1 x i64] }
%"(Int32 | Nil)" = type { i32, [1 x i64] }
%"(A | String)" = type { i32, [2 x i64] }
``````

As you can see the union type is represented with a tag and a field big enough for the actual value. Except for unions between a reference type and `Nil`, there the `nil` value is simply represented as a null pointer.

I was trying to find out the best way in crystal to model the enum ip address data type example from rust. https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html. Given rust’s enums are basically sum types.

``````    enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

``````

perhaps there is a better place/category to talk about this. But I am posting ways I would model it in crystal for completeness. Personally I wouldn’t mind hearing other people’s thoughts on this.

1. using static duck typing
``````struct V4
getter :octet1

def initialize(
@octet1 : Int32,
@octet2 : Int32,
@octet3 : Int32,
@octet4 : Int32)
end
end

struct V6
def initialize(@str : String)
end
end

alias Ip = V4 | V6

home = V4.new(127,0,0,1)
loopback = V6.new("::1")
``````
1. using module as interface
``````module Ip
end

class V4
include Ip
getter :octet1

def initialize(
@octet1 : Int32,
@octet2 : Int32,
@octet3 : Int32,
@octet4 : Int32)
end
end

class V6
include Ip

def initialize(@str : String)
end
end

home = V4.new(127,0,0,1)
loopback = V6.new("::1")
``````
1. using an abstract class
``````abstract class Ip
end

class V4 < Ip
getter :octet1

def initialize(
@octet1 : Int32,
@octet2 : Int32,
@octet3 : Int32,
@octet4 : Int32)
end
end

class V6 < Ip
def initialize(@str : String)
end
end

home = V4.new(127,0,0,1)
loopback = V6.new("::1")
``````

all three ways we can use the `case` to match the type

``````def show_ip(ip)
case ip
when V4 then "ip v4 #{ip.octet1}"
when V6 then "ip v6"
end
end
``````

Personally I would argue the aliased union, so your number 1, is the most idiomatic solution. But then I never understood why we added abstract classes…

My arguments for that would have nothing to do with the internal data presentation though. Crystal should be viewed as a high level language where you only care about these details when optimizing an identified performance bottleneck in your application. I would simply argue it makes the best use of the language features.

I’m sure it’s just an example, but just for the benefit of anybody stumbling on this in the future, of course stdlib sports a datatype for IP addresses: https://crystal-lang.org/api/0.34.0/Socket/IPAddress.html

1 Like

@epoch Only `alias` is the equivalent to Rust because in the next version apparently you will be able to do:

``````case ip
in V4
in V6
end
``````

and if you forget one of the cases you will get a compile error. Using classes or modules you will not get an error. And in Rust you get an error, so only `alias` is equivalent.

Ideally you would be able to use classes or modules with a sealed annotation but it seems not many like the idea so maybe it won’t happen. It would be nice if defining a closed group of types would be orthogonal to being able to check they are all covered in a case, like you can do in Scala and Kotlin, but not in Crystal.

1 Like

`alias union` is also my favourite usually but I believe all three are exhaustive.

on version 0.34.0 I am getting a warning on all three which I should be getting a compile error on the next version?

`````` 163 | case ip
^
Warning: case is not exhaustive.

Missing types:
- V6

A total of 1 warnings were found.
``````

Try this:

``````module Ip
end

class V4
include Ip
end

class V6
include Ip
end

class IpHolder
getter ip

def initialize(@ip : Ip)
end
end

holder = IpHolder.new(V4.new)
case holder.ip
when V4
when V6
end
``````

You get a warning where “Ip” is in the missing types.

Try now this:

``````class Ip
end

class V4 < Ip
end

class V6 < Ip
end

class IpHolder
getter ip

def initialize(@ip : Ip)
end
end

holder = IpHolder.new(V4.new)
case holder.ip
when V4
when V6
end
``````

Same warning.

If you do this:

``````ip = V4.new || V6.new
case ip
...
end
``````

it’s not the same because apparently the compiler types it as `V4 | V6`, not a `Ip`. You need to have an `Ip` to verify it.

`alias` is the only equivalent way to how it’s done in Rust.

1 Like

The semantics are changing in `0.35.0`.

``````case value
when x then ...
end
``````

Will now act like it did before `0.34.0`. Exhaustive case is now opt in via:

``````case value
in x then ...
in y then ...
end
``````
1 Like

Totally agree with you here Hank. I learned it the hard way thinking modules interfaces work like in golang (structural typing matching the shape) which sort of works in crystal for methods if you don’t annotate the type but unfortunately does not work for instance variables when you initialize.

Thank you for the link to sealed annotation. Keen to see where it goes in the future but It seems `alias` is the way to go for now.

@epoch unions in crystal are not tagged. They are implicit.

In Haskell you have `Maybe a = Just a | Nothing`, in crystal is `T | Nil` directly. So there is no need to tag a value with a `Just`.

But, since they are implicit, there are ways to code in a more tagged/explicit fashion if you like. But then you might need to fight at some point with method restrictions. A `Just a` is not an `a`.

1 Like