← Back to context

Comment by phkahler

3 days ago

>> Flix is a principled effect-oriented functional, imperative, and logic programming language...

>> Why Effects? Effect systems represent the next major evolution in statically typed programming languages. By explicitly modeling side effects, effect-oriented programming enforces modularity and helps program reasoning.

Since when do side effects and functional programming go together?

In Flix all effects are tracked by the type and effect system. Hence programmers can know when a function is pure or impure. Moreover, pure functions can be implemented internally using mutation and imperative programming. For example, in Flix, one can express a sort function that is guaranteed to be pure when seen from the outside, but internally uses a quick sort (which sorts in place on an array). The type and effect system ensures that such mutable memory does not escape its lexical scope, hence such a sort function remains observationally pure as seen from the outside.

(I am one of the developers of Flix)

  • Haskell can do the same kind of thing (local mutation), using the ST monad.

    It's usage is almost equivalent to using IORefs, except we can escape ST using runST to get back a pure value not in ST, which we cannot do for IO because there is no `IO a -> a`.

    There's no requirement to contain ST to a single function - we can split mutation over several functions, provided each one involved returns some `ST a` and their usage is combined with >>=.

    https://dl.acm.org/doi/pdf/10.1145/178243.178246

    • That's right. Locally scoped mutable memory in Flix is very similar to the ST Monad. The two major differences are: (a) Flix is in direct-style vs. monadic style and (b) we use a type and effect system.

      Note that there is no requirement that all mutation must occur within a single function. The requirement is that once you leave the lexical scope then all mutable memory associated with that scope become unreachable. Mutation can certainly span over multiple functions.

FP isn't really about eliminating side effects. Controlled effects are fine. That's what an effect system does.

Avoiding side effects is really just a side effect (pun intended) of older programming language technology that didn't provide any other way to control effects.

  • Arguably FP really is about eliminating side effects.

    The research has sprung out of lambda calculus where a computation is defined in terms of functions (remember: Functional programming).

    Side effects can only be realized by exposing them in the runtime / std-lib etc. How one does that is a value judgement, but if a term is not idempotent, then you arguably does not have a functional programming language anymore.

    • You gotta ask the question: why does FP care about eliminating side effects? There are two possible answers:

      1. It's just something weird that FP people like to do; or

      2. It's in service of a larger goal, the ability to reason about programs.

      If you take the reasonable answer---number 2---then the conclusion is that effects are not a problem so long as you can still reason about programs containing them. Linear / affine types (like Rust's borrow checker) and effect systems are different ways to accommodate effects into a language and still retain some ability to reason about programs.

      No practical program can be written without effects, so they must be in a language somewhere.

      More here: https://noelwelsh.com/posts/what-and-why-fp/

      2 replies →

    • If you start with lambda calculus you don't have effects in the first place, so there's nothing to eliminate. Lambda calculus and friends are perfectly reasonable languages for computation in the sense of calculation.

      A better way to think about general-purpose functional programming is that it's a way to add effects to a calculation-oriented foundation. The challenge is to keep the expressiveness, flexibility and useful properties of lambda calculus while extending it to writing interactive, optimizable real-world programs.

    • To retain referential transparency, we basically need to ensure that a function provided the same arguments always returns the same result.

      A simple way around this is to never give the same value to a function twice - ie, using uniqueness types, which is the approach taken by Clean. A uniqueness type, by definition, can never be used more than once, so functions which take a uniqueness type as an argument are referentially transparent.

      In Haskell, you never directly call a function with side effects - you only ever bind it to `main`.

      Functions with (global) side effects return a value of type `IO a`, and the behavior of IO is fully encapsulated by the monadic operations.

          instance Monad IO where
              return :: a -> IO a
              (>>=) :: IO a -> (a -> IO b) -> IO b    -- aka "bind"
      

      return lifts a pure value into IO, and bind sequences IO operations. Importantly, there cannot exist any function of type `IO a -> a` which escapes IO, as this would violate referential transparency. Since every effect must return IO, and the only thing we can do with the IO is bind it, the eventual result of running the program must be an IO value, hence `main` returns a value of type `IO ()`.

          main :: IO ()
      

      So bind encapsulates side effects, effectively using a strategy similar to Clean, where each `IO` is a synonym of some `State# RealWorld -> (# State# RealWorld, a #)`. Bind takes a value of IO as it's first argument, consumes the input `State# RealWorld` value and extracts a value of type `a` - feeds this value the next function in the sequence of binds, returning a new value of type `IO b`, which has a new `State# RealWorld`. Since `bind` enforces a linear sequencing of operations, this has the effect that each `RealWorld` is basically a unique value never used more than once - even though uniqueness types themselves are absent from Haskell.

    • Lisp always had side effects and mutability, and it's the canonical FP language, directly inspired by lambda calculus. To be fair, before Haskellers figured out monads, nobody even knew of any way to make a FP language that's both pure and useful.

      2 replies →

Functional programming à la Haskell has always been about making effects controllable, explicit first-class citizens of the language. A language entirely without effects would only be useful for calculation.

The talk about "purity" and "removing side effects" has always been about shock value—sometimes as an intentional marketing technique, but most often because it's just so much easier to explain. "It's just like 'normal' programming but you can't mutate variables" is pithy and memorable; "it's a language where effects are explicitly added on top of the core and are managed separately" isn't.