Hello, you are not alone to “resonate” with the ideas presented in this great book
When I tried to reproduce in Elixir some on the examples given, I used the “all is struct” approach and the typed_struct library.
Then played with VSCode and dialyzer (using the dialyzer underspecs
and overspecs
options).
Here is the code (just a POC ! ;-)) :
defmodule OrderTakingTypes do
defmodule OrderLines do
use TypedStruct
typedstruct do
field(:lines, list(OrderLine.t()), default: [])
end
end
defmodule OrderLine do
use TypedStruct
typedstruct do
field(:product_code, String.t(), enforce: true)
field(:quantity, number(), default: 0)
field(:price, number(), default: 0)
end
end
defmodule ShippingAddress do
use TypedStruct
typedstruct enforce: true do
field(:town, String.t())
field(:zip_code, String.t())
end
end
defmodule Order do
use TypedStruct
typedstruct enforce: true do
field(:shipping_address, ShippingAddress.t())
field(:order_lines, list(OrderLine.t()))
end
end
defmodule ValidatedOrder do
use TypedStruct
typedstruct enforce: true do
field(:order, Order.t())
field(:validation_date, DateTime.t())
end
end
end
defmodule TypeDemo do
alias OrderTakingTypes.{OrderLines, OrderLine, ShippingAddress, Order, ValidatedOrder}
def send() do
%OrderLines{
lines: [
%OrderLine{price: 10, product_code: "SKU-0001", quantity: 5}
]
}
|> send_to(%ShippingAddress{zip_code: "69000", town: "Lyon"})
end
@spec send_to(OrderLines.t(), ShippingAddress.t()) :: :ok
def send_to(%OrderLines{lines: order_lines}, %ShippingAddress{} = address) do
%Order{
shipping_address: address,
order_lines: order_lines
}
|> validate_order()
|> process_order()
end
@spec validate_order(Order.t()) :: ValidatedOrder.t() | ErrorResponse.t()
def validate_order(%Order{order_lines: order_lines} = order) when length(order_lines) > 0 do
%ValidatedOrder{
order: order,
validation_date: DateTime.utc_now()
}
end
def validate_order(%Order{order_lines: order_lines}) when length(order_lines) == 0,
do: %ErrorResponse{code: 500, message: "OrderLines cannot be empty"}
def validate_order(_), do: %ErrorResponse{code: 500, message: "Unknown error"}
@spec process_order(ValidatedOrder.t() | ErrorResponse.t()) :: :ok
def process_order(%ValidatedOrder{
order: %Order{shipping_address: %ShippingAddress{town: town, zip_code: zip_code}}
}),
do: IO.puts("Processed shipping to #{zip_code}, #{town}")
def process_order(%ErrorResponse{code: code, message: message}),
do: IO.puts("Error occured with code '#{code}' and message '#{message}'")
def process_order(_), do: IO.puts("Weird error")
end
Depending of the type of error you introduce (missing return type in @spec
for instance) and on the Dialyzer options, it will be too verbose or too silent.