[OT] Polymorphic effects in Flix

Flix language handles effects nicely (only purity, it seems here): https://flix.dev/#/blog/taming-impurity-with-polymorphic-effects/

(But I don't like the effects syntax much, I think a nicer syntax could be invented. The semantics seems OK to me).

2 Likes

This sounds like a generalization of "rethrowing" annotations of exceptions from certain languages (I have the most experience with Swift), and it seems more principled than those; on the surface it's even kind of neat.

However, I've found that in practice, polymorphism over effects quickly becomes painful and redundant in the setting of higher-order functions. To elaborate on that:

  • Suppose there is a higher-order function h taking a functional argument f
  • The functional argument f may or may not be effectful
  • Most often, one wants the effectfulness of h to match that of f
  • If the default is "pure" (or, in the special case of exceptions, "non-throwing"), then it is almost always necessary to write out the effect polymorphism (or the "rethrows" annotations), otherwise h will not be usable with an effectful f.
    • (On the other hand, if the default was impure, that would:
      1. be weird and contrary to principles of functional languages as well as Rust, and
      2. either make a pure f unusable by a non-polymorphic h or result in a high number of purity violation false positives.)
  • So, now it becomes an API design good practice to always or at least by default make higher-order functions effect-generic.
  • At this point, the annotation becomes redundant, while APIs that don't consider this problem become unusable with some functional arguments that they should be able to accept otherwise.

I've written about this problem in more detail previously: one, two.

I suppose I have trouble accepting that there is even a notion of something being default here when it comes to higher-order functions, because pure/impure/polymorphic all appear to be distinct syntactic categories.

with the examples showing f: Unit -> Unit being an alias for f: Unit -> Unit & Pure, and f: Unit ~> Unit being an alias for f:Unit -> Unit & Impure, and f: Unit -> Unit & _ presumably being polymorphic, that is the choice between Pure, Impure, and Polymorphic is in all 3 cases explicit (again in this case that is dealing with higher-order functions).

painful, sure but I don't really see how any of the 3 distinct things could redundant.

Probably worth mentioning that pre-1.0 Rust used to have purity annotations like this, but ended up abandoning them because of exactly this problem forcing everyone to opt into "impurity" everywhere and making the annotation meaningless.

2 Likes

They give an example with even two different input functions, f and g:

def mapCompose(f: a -> b & e1, g: b -> c & {{(not e1) / e2}}, xs: List[a]): ... = ...

I'm aware that this is not an unavoidable law, it's "only" the common case. That there are other, less frequent cases doesn't matter, because the baggage and the frustration associated with the common case still remains.