Skip to content

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.
blossom
HelloWorld -> Log.Info("Hello World!")

INFO

Functions with this schema are called procedures.

PrintName
  • Has a name (String) argument.
  • Returns None.
  • Throws no errors.
blossom
PrintName :: (name: String) -> Log.Info("My name is {name}")

INFO

The :: symbol denotes the function schema.

Add
  • Has x (Int) and y (Int) arguments.
  • Returns Int.
  • Throws no errors.
blossom
Add :: (x: Int, y: Int) : Int -> x + y
Divide
  • Has x (Float) and y (Float) arguments.
  • Returns Float.
  • Throws @DivisionError.
blossom
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) and divisor (Int) arguments.
  • Returns (Int, Int).
  • Throws @DivisionByZero.
blossom
DivideWithRemainder
    ::
      (dividend: Int, divisor: Int)
      : (Int, Int)
      ! @DivisionByZero
    -> {
      match divisor -> {
          0 => throw @DivisionByZero
          _ => (dividend / divisor, dividend % divisor)
      }
    }
CalculateArea
  • Has width (Float) and height (Float) arguments.
  • Returns Float.
  • Throws @NegativeSideError and @ZeroAreaError.
blossom
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

blossom
IncrementAndDouble
  :: (x: Int) : Int
  -> { Double(Increment(x)) }
blossom
IncrementAndDouble
  :: (x: Int) : Int
  -> { x |> Increment |> Double }
blossom
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:

  1. The template is violated;
  2. The rendered types are incompatible with the implementation.

Example

blossom
Add :: t <> (x: t, y: t) : t -> x + y

The function Add has a type parameter t and takes two parameters x and y, both of type t, returns a value of type t, and its implementation adds x and y.


Add(1, 1)
blossom
Add(1, 1)

OK

  1. Both arguments are integers (Int), so t resolves to Int;
  2. The + operator is defined for the Int type, and thus compatible with the implementation.
Add(0.5, 0.5)
blossom
Add(0.5, 0.5)

OK

  1. Both arguments are floating-point numbers (Float), so t resolves to Float;
  2. The + operator is defined for the Float type, and thus compatible with the implementation.
Add(None, None)
blossom
Add(None, None)

ERROR

  1. Both arguments are None, so t resolves to None;
  2. The + operator is not defined for the None type, making the implementation incompatible with the rendered type.
Add(1, 0.5)
blossom
Add(1, 0.5)

ERROR

  1. Arguments have different types (Int and Float), which violates the template;
  2. 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.

blossom
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:

  1. Makes the function's schema immediately visible;
  2. 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.