By Samuel Belko, published on April 26, 2025.
In this post, I would like to discuss how a type system can help us deal with complexity of implementing a generic optimization application. I illustrate the ideas on a concrete example, developed in my master thesis on design optimization of a hybrid railway vehicle.
A hybrid vehicle uses some combination of different power components, combining diesel engines, batteries, fuel cells and electrical network. There are vehicles that are just electric or use only batteries or use a combination of batteries with an electrical network, or combine diesel engines with batteries and an electrical network etc.
A user can choose which components a vehicle contains, and based on that, a corresponding optimization model is generated and solved. As the same models of power components are used irrespectable of in which combination they appear, we can write a generic software that will combine the models of available components automatically, to create a corresponding scenario model. The approach I would like to discuss relies on maybe data types, modelling availabilities of components in an input. If a component is not available in an input configuration, it should also not be avalable in the scenario model and also not be available in the returned solution. Hence, the idea is to propage the configuration definition, encoded via a type system, throughout all stages of the execution. This design pattern closely resambles a maybe monad, where some functions may fail, and their failure is propagated thoughout execution. In our use case for a maybe monad, only the user "can fail" to provide a component, and the intermediate steps cannot fail by themselves.
In the following, let and be types. We denote a sum type of and by , and a product type of and by . If you imagine a type as a set containing all instances of that type, then you can think of the sum type as a disjoint union of and , and of the product type as a set product of and . Sum types are generally known under the name enums, and product types under the names tuples, records or structs.
The important high level concept is the following. A function defined on a sum type , requires either an instance of type , or an instance of type . A function defined on a product type , requires both instances, one of type and one of type Somewhat dually, the implementation of a function defined on a sum type , has to deal with both cases, as it doesn't know, if a caller passed an instance of type or an instance of type . And the implementation of a function defined on a product type , can choose to discart a part of the input. For example, use the input instance of type for projecting out an instance of type , and then call some function defined on type .
Furthermore, we define a maybe type of a type as , where is a singleton type. A singleton type has only one instance. Hence, an instance of type is either an instance of type , or the only instance of type .
In our case, we define a singleton type called . For a type , we construct a new type , creating an option to not specify a component related data.
Let denote the input type for a component , denote the type managing the model of a component and denote the type representing the part of a solution corresponding to a component . On a very high level, omitting further inputs, we obtain the following type definitions for my implementation,
The interpretation is that a scenario manages inputs for all components, and if a component is not available, then we pass a special instance of type . To obtain an optimization problem, for each available component, we generate a model and collect them inside a instance. We propagate the component availability information in a obvious way, if a component was not available in a scenario, it is also not avaible in a instance. After we have generated a instance, we can try to solve it. If we succeed, we return an instance of a type. We propagate the component availability information as before, mapping not available component models to not available component solutions.
For example, in a programming language Julia, we could define abstract data types,
abstract type AbstractComponent end
abstract type AbstractComponentProblem end
abstract type AbstractComponentSolution end
and for each component , set to be a subtype of AbstractComponent
, to be a subtype of AbstractComponentProblem
and to be a subtype of AbstractComponentSolution
. Then, we introduce the singelton type as follows.
struct NotAvailable end
Below, we define a parametric type MaybeAvailable{T}
mapping a type , which is modeling either a component input or a component model or a component solution, into a type .
const MaybeAvailable{T} = Union{
NotAvailable,T
} where {T<:Union{AbstractComponent,AbstractComponentProblem,AbstractComponentSolution}}
We define a useful utility function isavailable
for checking whether an instance of type is actually available or not.
isavailable(x::MaybeAvailable) = !isa(x, NotAvailable)
In the implementation of the optimization software, I repeatedly rely on the following pattern, to realize the propagation mechanism.
maybe_processed_component = if isavailable(maybe_component)
# process component
else
NotAvailable()
end
The above if else
statement is matching maybe_component
against the two cases of the disjoint union type. Either maybe_component
is available, and we proceed to the next stage, or maybe_component
is not available, and we return an instance of type .
Now, this is nothing else than an ad hoc definition of a composition in a maybe monad. But, my implementation is not composing pure functions, and so the monadic pattern is just a rough analogy. Overall, we can trace back the structural ideas to functional programming, but implemented in a multi-paradigm language.
What I would like to especially highlight, is that the maybe monad idea was repurpused to help us deal with complexity arising in having different components being available in the input. Yet, still, the same principles proved useful, even though the motivation for a maybe monad is to model partial functions, which may fail during execution. I find the idea very elegant, to "compose" the user providing an input with the program, and model the user as a "function which may fail" to provide input data. This deliberate failure of the user is then propagated through the program in a consistent manner.
If you like to learn more about the math behind functional programming, I can recommend reading the excellent lecture notes for the course Programming with Categories, held at MIT in 2020.
I hope this blog post illustrated an example use of maybe data types in an industrial software application, and that you have learned something new.