Functions
Functions are the core building blocks of Blossom programs, encapsulating reusable code that accepts arguments, returns values and explicitly declares potential errors.
Signatures
A function signature tells you everything you need to know about how to use a function: what inputs it expects, what output it produces and what errors it might encounter.
Signatures
- Name: The function's identifier. This is how you call the function in your code.
- Parameters: These are the values the function takes as input. Each parameter has a name and a type. If there are no parameters, the function effectively takes no input.
- Return: This is the type of the value the function returns. If the function doesn't explicitly returns anything, its return type is
None
. - Errors: These are the types of errors the function might throw during execution. Listing these errors is a way of saying that the function might not always produce a normal output; it might instead produce an error. This is a way to handle potential side effects.
Examples
HelloWorld
- Has no arguments.
- Returns
None
. - Throws no errors.
HelloWorld -> Log.Info("Hello World!")
INFO
Functions with this schema are called procedures.
PrintName
- Has a
name
(String
) argument. - Returns
None
. - Throws no errors.
PrintName :: (name: String) -> Log.Info("My name is {name}")
INFO
The ::
symbol denotes the function schema.
Add
- Has
x
(Int
) andy
(Int
) arguments. - Returns
Int
. - Throws no errors.
Add :: (x: Int, y: Int) : Int -> x + y
Divide
- Has
x
(Float
) andy
(Float
) arguments. - Returns
Float
. - Throws
@DivisionError
.
Divide
::
(x: Float, y: Float)
: Float
! @DivisionByZero
-> {
match (x, y) -> {
(_, 0) => throw @DivisionByZero
(x, y) => x / y
}
}
INFO
The !
symbol denotes the function errors.
INFO
Function signatures can be arranged into multiple lines, as shown in the example above.
DivideWithRemainder
- Has
dividend
(Int
) anddivisor
(Int
) arguments. - Returns
(Int, Int)
. - Throws
@DivisionByZero
.
DivideWithRemainder
::
(dividend: Int, divisor: Int)
: (Int, Int)
! @DivisionByZero
-> {
match divisor -> {
0 => throw @DivisionByZero
_ => (dividend / divisor, dividend % divisor)
}
}
CalculateArea
- Has
width
(Float
) andheight
(Float
) arguments. - Returns
Float
. - Throws
@NegativeSideError
and@ZeroAreaError
.
CalculateArea
::
(width: Float, height: Float)
: Float
! @(ZeroAreaError, NegativeSideError)
-> {
match (width, height) -> {
(w, h) where (w < 0 | h < 0) -> throw @NegativeSideError
(0, _) -> throw @ZeroAreaError
(_, 0) -> throw @ZeroAreaError
(w, h) -> w * h
}
}
Pipelines
The pipeline operator |>
allows for chaining function calls in a readable syntax. Each function in the pipeline receives the result of the previous function as its input.
The error handling operator !>
is used within pipelines to handle errors at each step. It allows for local error handling without breaking the pipeline chain.
Examples
IncrementAndDouble
:: (x: Int) : Int
-> { Double(Increment(x)) }
IncrementAndDouble
:: (x: Int) : Int
-> { x |> Increment |> Double }
ProcessUser
:: (user: User) : ProcessedUser ! @(ValidationError, ProcessingError)
-> {
user
|> Validate
!> { @InvalidData => throw @ValidationError }
|> UpdateUser
!> { @UpdateFailed => throw @ProcessingError }
}
Templating
Blossom's philosophy values explicit and self-documented code. However, there are scenarios where we want to write functions that can operate on different types while maintaining type safety. Templating provides a way to write generic functions without sacrificing strict typing or code clarity.
Type templating uses the <>
operator to declare type parameters that can be rendered to concrete types when the code is compiled.
WARNING
Given that all the types are evaluated during compilation, a compilation error will be thrown if:
- The template is violated;
- The rendered types are incompatible with the implementation.
Example
Add :: t <> (x: t, y: t) : t -> x + y
The function
Add
has a type parametert
and takes two parametersx
andy
, both of typet
, returns a value of typet
, and its implementation addsx
andy
.
Add(1, 1)
Add(1, 1)
OK
- Both arguments are integers (
Int
), sot
resolves toInt
; - The
+
operator is defined for theInt
type, and thus compatible with the implementation.
Add(0.5, 0.5)
Add(0.5, 0.5)
OK
- Both arguments are floating-point numbers (
Float
), sot
resolves toFloat
; - The
+
operator is defined for theFloat
type, and thus compatible with the implementation.
Add(None, None)
Add(None, None)
ERROR
- Both arguments are
None
, sot
resolves toNone
; - The
+
operator is not defined for theNone
type, making the implementation incompatible with the rendered type.
Add(1, 0.5)
Add(1, 0.5)
ERROR
- Arguments have different types (
Int
andFloat
), which violates the template; - The
+
operator is not compatible with different types.
Schemas
Function schemas allow the creation of function templates within Blossom, promoting reusability with type safety. Function schemas are defined using the :>
operator.
Example
Adder :> (Int, Int) : Int
Add :: Adder<x, y> -> x + y
INFO
Schemas also enable the definition of anonymous functions within blossom. See more in the next chapter.
Higher-order functions
Conceptually, a higher-order function is a function that does at least one of the following:
- Takes one or more functions as parameters;
- Returns a function as its result.
Higher-order functions work in conjunction with anonymous functions - functions with a temporary lifespan that are defined inline where they are used.
Example
A classic example is the Map
function, which takes a transformation function as a parameter to process each element in a list.
Mapper :> t <> t : t
Map
:: t <> (list: List<t>, fn: Mapper<t>) : List<t>
-> {
match list -> {
[] => []
[x, ...xs] => [fn(x), ...Map(xs, fn)]
}
}
DoubleNumbers
:: (numbers: List<Int>) : List<Int>
-> Map(numbers, Mapper<n> -> n * 2 )
INFO
In Blossom, anonymous functions must be defined using a schema (like Mapper<t>
above).
This explicit typing serves two purposes:
- Makes the function's schema immediately visible;
- Helps developers understand the purpose of the anonymous function without checking the parent function's signature.
WARNING
Function schemas, like composite types, must be defined outside of function signatures.
This ensures code clarity and promotes type reuse.