Use exceptions vs other patterns?

I think there’s a misunderstanding here. Not every use of exceptions discouraged. You earlier cited the following:

it is not recommended to use exception handling as part of the normal workflow in Julia

I understand this statement as “if you, as a programmer, expect the user to handle certain exception in a certain way, then you should return a proper value”. Signaling unexpected situations through exceptions, to me, does not contradict that recommendation.

2 Likes

Ah yes, you’re right - and that makes sense. In fact, the full quote is this:

In contrast to Python, it is not recommended to use exception handling as part of the normal workflow in Julia (compared with Python, Julia is faster at ordinary control flow but slower at exception-catching).

The word Python here is key. I didn’t pick up on this before.

In Python, there is this philosophy of ask forgiveness not permission which really means to make frequent use of exceptions as part of the regular control flow. I don’t always agree with this, it depends a lot on the context.

But yes I agree, with this in mind it now makes perfect sense. The author must be assuming that someone coming from Python will try to use exceptions just about everywhere for control flow, which obviously doesn’t make a lot of sense in Julia because of the performance implications. Before I was using Python regularly, I was originally a C++ developer, so that’s why I didn’t realize the full context here.

Thanks for your comment.

2 Likes

Sometimes in functional programming communities you will see a design recommendation like the following:

  • Don’t just habitually use Maybe and Result everywhere. Instead use custom data types that capture the business/application logic.

For example, here’s an excerpt from the Elm language guide that advises against over using Maybe:

For example, say we have an exercise app where we compete against our friends. You start with a list of your friend’s names, but you can load more fitness information about them later. You might be tempted to model it like this:

type alias Friend =
  { name : String
  , age : Maybe Int
  , height : Maybe Float
  , weight : Maybe Float
  }

All the information is there, but you are not really modeling the way your particular application works. It would be much more precise to model it like this instead:

type Friend
  = Less String
  | More String Info

type alias Info =
  { age : Int
  , height : Float
  , weight : Float
  }

This new model is capturing much more about your application. There are only two real situations. Either you have just the name, or you have the name and a bunch of information.

I’m going to translate this into Julia, with an extra twist.

abstract type Friend end

struct Unknown end
struct PreferNotToSay end

struct PartialFriend <: Friend
    name::String
end

struct Info
    age::Int
    height::Float64
    weight::Float64
end

struct ShyInfo
    age::Int
    height::Float64
end

struct NotShyFriend <: Friend
    name::String
    info::Info
end

struct ShyFriend <: Friend
    name::String
    info::ShyInfo
end

name(f::PartialFriend) = f.name
name(f::NotShyFriend) = f.name
name(f::ShyFriend) = f.name

weight(::PartialFriend) = Unknown()
weight(::ShyFriend) = PreferNotToSay()
weight(f::NotShyFriend) = f.info.weight

_print_weight(name, ::Unknown) = println("$name's weight is unknown.")
_print_weight(name, ::PreferNotToSay) = println("$name prefers not to disclose their weight.")
_print_weight(name, weight::Float64) = println("$name's weight is $weight.")

print_weight(f::Friend) = _print_weight(name(f), weight(f))

Note how the _print_weight methods dispatch on the different possible return types of the weight function. This is pretty similar to pattern matching on a sum type in a case statement.

The individual weight methods are actually type-stable, but the return type of a call to weight in some code might be a union type, like when you call weight on an object from a heterogenous collection, like this,

friends = # A `Vector{Friend}`
weight(friends[2])

where the return type of weight(friends[2]) is

Union{Float64, PreferNotToSay, Unknown}

There are known performance issues with heterogeneous collections in Julia, but semantically the design pattern I’ve outlined in the example above is quite similar to the design approach in a functional language with sum types (i.e. create custom data types that represent the business logic—don’t just habitually use Maybe and Result). One of the main reasons to use sum types in Julia is to avoid the performance pitfalls of heterogeneous collections. If you really need sum types, take a look at the new Moshi.jl library.

2 Likes