[HN Gopher] Exotic Programming Ideas, Part 3: Effect Systems ___________________________________________________________________ Exotic Programming Ideas, Part 3: Effect Systems Author : psibi Score : 180 points Date : 2020-11-22 15:56 UTC (7 hours ago) (HTM) web link (www.stephendiehl.com) (TXT) w3m dump (www.stephendiehl.com) | siraben wrote: | I highly recommend Oleg Kiselyov's talk titled "Having an | Effect"[0] in which he talks about | | - purely functional model of computation and its pitfalls | | - Actor model, effectful programming with requests and responses | and an implementation in Haskell. | | - denotational semantics and combining effects. Once you have a | model of your language, what if you want to extend it by adding | another effect? It forces you to rewrite the semantics (and thus | any interpreter of your language) completely. Taking using the | effects-as-requests viewpoint, only the request and handler for | an effect needs to be added or changed, and the rest untouched. | This is known as "stable denotations". | | - really evaluating what it means for an expression to be pure or | effectful. Even variable reference should be considered an | effect. | | - different scoping rules in lambda calculus can be expressed in | terms of effects! Creating a dynamic closure is not effectful, | though applying it usually is, OTOH, creating a lexical closure | is effectful but using it is not. | | I think Haskell provides a good example of how a purely | functional language can still express effectful computation. | through a monadic interface. Though monad transformers have their | share of problems when heavily nested (n^2 instances, | performance), various effect system libraries are gaining | traction.[2] On the bleeding edge of research there's languages | like Frank[1] where the effect system is pervasive throughout the | language. | | [0] https://www.youtube.com/watch?v=GhERMBT7u4w | | [1] https://github.com/frank-lang/frank | | [2] Implementing free monads from scratch, | https://siraben.github.io/2020/02/20/free-monads.html | chrisdone wrote: | The recent Unison language uses the effect system described in | the Frank language https://www.unisonweb.org/ | cultus wrote: | Also see Oleg's "Effects without Monads: Non-determinism -- | Back to the Meta-Language" | | https://arxiv.org/abs/1905.06544 | merelydev wrote: | Source: | https://www.reddit.com/r/programming/comments/7wbtg/who_is_o... | - Oleg eats lambdas for breakfast - The Y | combinator fears Oleg - Oleg knows all the programs | that halt on a Turing machine - Oleg is the guy | that Chuck Norris goes to when he has an algorithm complexity | question - Oleg reprograms his own DNA with Scheme | macros - All of Oleg's Haskell programs are well- | typed by definition - Oleg can read C++ compiler | error messages. - Oleg built his house out of | Prolog relations - Oleg speaks machine lanugage | - Oleg once turned a zero into a one. - Emacs? Vi? | Oleg wills files into existance - Oleg has the | Haskell98 Report memorized. In Binary. In UTF-EBCDIC | - Oleg can write unhygienic syntax-rules macros. - | Sometimes Recursion gets confused when Oleg uses it. | patrec wrote: | > Oleg can write unhygienic syntax-rules macros. | | Not only can he do it, he even wrote a paper about it: https: | //citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.36... | siraben wrote: | IMO implementing hygienic macros properly is one of the | hardest tasks in programming languages, especially since | variable capture can cause subtle bugs. | patrec wrote: | Well, certainly no one seems to understand how e.g. | syntax-case works. But my impression is that macro | hygiene in itself is a solution looking for a problem. | The key advantage e.g. racket's macro system has over | clojure or common lisp is not hygiene but being | sufficiently well structured and rich to allow proper | tooling. Good error messages with accurate locations >> | macro hygiene. | kazinator wrote: | Macro hygiene is a solution to the problem of functions | being in the same namespace as variables, together with | standard, oft-needed library functions having short names | that are easily chosen as variable names. | akiselev wrote: | I think in theory they're orthogonal but in practice the | two go together. It's all fine and dandy when you've got | a restricted set of battle tested macros from a single | library interacting in real world code, but it rapidly | breaks down when you've got application authors of | various skill sets all contributing their own macros | because the dare not touch the ones that came before. | Macro hygiene provides the equivalent of type level | guarantees to metaprogramming scope. | mpweiher wrote: | And maybe Macros aren't the right kind of metaprogramming | mechanism... | deckard1 wrote: | > macro hygiene in itself is a solution looking for a | problem | | No. Unless you're not familiar with Lisp-1 vs Lisp-2. In | Scheme, you would have to GENSYM every variable in | addition to every function you call within a macro. | Whereas in Common Lisp you just need to GENSYM the | variables. That's the real reason Scheme doesn't use | DEFMACRO. | | I'm not personally a fan of any hygienic macro system | because learning a new language defeats the purpose and | elegance of Lisp macros in the first place. But then | again, outside of personal projects and academic | exercises, no one should be using macros. Messing with | fundamental semantics of a language while other | developers are working on the same project will certainly | make you a ton more enemies than friends. I still have a | grudge against the guy that used Ruby's method_missing | and I spent an entire day hunting down a method that | didn't exist. When I figured it out, I don't think I've | ever been so pissed at someone before. | fiddlerwoaroof wrote: | The problem with Ruby's method missing is mostly that | Ruby development generally doesn't happen in an | environment where discovering the existence of that | method is easy. Macros are even better and tools like the | slime macrostep expander make them relatively easy to | deal with. | The_rationalist wrote: | Arrow Fx implement this idea for Kotlin -> https://arrow- | kt.io/docs/fx/ | ianbicking wrote: | I've had this idea of "dynamic returns" (akin to dynamic scope) | in my head for a while. Reading this, it feels like a dynamically | typed companion to effect systems. | | The idea of a dynamic return is just to give a formal way to | accumulate things during a set of function calls, without having | every function to be aware of what might be happening. In Python | context managers are often used for this (e.g., | contextlib.redirect_stdout to capture stdout), but thinking about | it as another kind of return value instead of "capturing" would | be an improvement IMHO. (You have to "capture" when hardcoded | imperative code later needs to be retrofitted, but as it is | retrofitting is all we have.) | | But dynamic returns aren't quite like an effect system unless you | also create something more-or-less like a transaction or a | changeset. We usually think about transactions as simply a way to | rollback in case of an error, but as a changeset there's all | kinds of interesting auditing and logging and debugging | possibilities. E.g., if your effect is writing to stdout, you | could rewrite all those changes (e.g., apply a filter, or add a | text prefix to each line). | andrewflnr wrote: | I don't even see anything essentially type-based in effect | systems. You could totally have effect-style APIs in dynamic | languages, they just wouldn't be tracked and checked up front. | I expect this has already been done a few times in Scheme. | marcosdumay wrote: | > E.g., if your effect is writing to stdout, you could rewrite | all those changes | | Could you? Once you write something into stdout, as far as your | program knows it could already be sent across the world and | turned into a set of bank transactions, or missiles fired. | transfire wrote: | I believe Kitten is another language exploring this area. | PaulHoule wrote: | "Non-termination is an Effect" | SomeHacker44 wrote: | ...that cannot be determined by the system (and must be | specified by the user). | daanx wrote: | Just to add to this: Koka can (obviously :-)) not always | determine if a function will terminate or not so it generally | adds a `div` effect whenever there is the possibility of | infinite recursion. | | However, since most data types in Koka are inductive, any | recursion over such inductive data types are still inferred | to be always terminating. | | In practice, it looks like about 70% of a typical program can | usually be `total`, with 20% being `pure` (which is | exceptions + divergence as in Haskell), and the final 10% | being arbitrary side effects in the `io` effect. | daanx wrote: | (Daan here, creator of [Koka](https://github.com/koka- | lang/koka) | | This is an interesting point and comes down to the question -- | what is an effect really? I argue that effect types tell you | the type signature of the mathematical function that models | your program (the denotational semantics). For example, the | function fun sqr(x : int) : total int { x*x } | | has no effect at all. The math function that gives semantics to | `sqr` would have a type signature that can be directly derived | from the effect type: [[int -> total int]] = Z | -> Z | | (with Z the set of integers). Now, for a function that raises | an exception, it would be: [[int -> exn int]] = | Z -> (Z + 1) | | That is, either an integer (Z), _or_ (+), a unit value (1) if | an exception is raised. Similarly, a function that modifies a | global heap h, would look like: [[int -> st<h> | int]] = (H,Z) -> (H,Z) | | that is, it takes an initial heap H (besides the integer) and | returns an updated heap with the result. | | Now, non-termination as an effect makes sense: a function like | "turing-machine" may diverge, so: [[int -> div | int]] = Z -> Z_\bot | | That is, its mathematical result is the set of integers (Z) | together with an injected bottom value that represents non- | termination. (note: We don't use "Z + \bot" here since we | cannot distinguish if a function is not terminating or not (in | contrast to exceptions)). | | In a language like Haskell every value may not terminate or | cause an exception -- that is a value `x :: Int` really has | type `Int_\bot`, and we cannot replace for example `x*0` with | `0` in Haskell. | | Note that in almost all other languages, the semantic function | is very complex with a global heap etc, any function `[[int -> | int]]` becomes something like `(H,Z_32) -> (H,(Z_32 + | 1))_\bot`. This is the essence of why it much harder for | compilers and humans to reason about such functions (and why | effect types can really help both programmers and compilers to | do effective local reasoning) | skybrian wrote: | I'm wondering if you've found it useful in practice to | distinguish between total and possibly-diverging functions? | | It seems like it's the sort of thing that's useful in | something like Agda, where you use the existence of a | function (without running it) to prove that its result | exists. (The type is inhabited.) Or so I've read; I haven't | used it. | | But if you're going to run the program, you typically want to | know if a function will return promptly, and a total function | could still spin for a million years calculating something, | in a way that's indistinguishable in practice from diverging. | opnitro wrote: | I have found total checks in languages like idris helpful, | but really only for catching small mistakes that I've made | (for example, recursing on the original argument) | daanx wrote: | _In practice_ , I have not (yet) found many great use cases | for the distinction in Koka. It is nice to have "total" | functions, but "pure" (exceptions+divergence) is still a | good thing (and what Haskell gives you). And like you say, | in practice we can easily have functions that just take a | long long time to compute. | | Still, it is a good extra check and I can see more use for | the `div` effect for future verification tools where total | functions can be used as a predicates (but non-terminating | ones cannot). | skybrian wrote: | Good to know. At compile or verification time, it seems | like you could get the same problem again, though? We | want our compiles to finish promptly, and a slow, total | predicate could hang. | | Which is to say, a possibly-diverging function seems okay | to use even by a verification tool, so long as it | finishes quickly in practice. Having some notion of | analysis-time-safe predicates seems useful but it doesn't | seem to map cleanly to being able to prove termination? | tomp wrote: | I never understood this one. I mean obviously a kind compiler | should warn you against using while true { } | | and while i < 5 { i---; } | | but in general, I don't find distinction between "this infinite | loop terminates" (e.g. an event loop) and "this code obviously | terminates but not in the lifetime of this universe" (e.g. | computing Ackermann(5)) to be that useful. | creata wrote: | It's primarily useful for theorem proving, where a | nonterminating argument corresponds to circular or otherwise | unfounded reasoning. | | It's also useful because, in practice, we don't accidentally | write code that "obviously terminates but not in the lifetime | of this universe" as often as we accidentally write | nonterminating code, so a lot of mistakes can still be caught | by termination checking. | aozgaa wrote: | > As far as I can tell no one uses this language [Koka] for | anything, however it is downloadable and quite usable to explore | these ideas. | | I believe the typesetting tool Madoko[1] is implemented in Koka, | though in fairness Daan Leijen developed both Koka and Madoko. | | [1] https://github.com/koka-lang/madoko | klodolph wrote: | There's some interesting research and ideas here, but it does | seem like monads "ate everything for lunch" back in the late | 1990s and 2000s when it comes to encoding effects, probably | because monads are a bit more ergonomic (which seems like a weird | thing to say, given the reputation monads have for being abstract | nonsense). So effect systems didn't get as much research as | everyone was interested in monads, and now that monads have dried | up a bit as a field of research for encoding effects, I'm | interested to see what other systems people invent. | smegma2 wrote: | I was curious about this function: fun addRefs( | a : forall<h> ref<h,int>, b : forall<h> ref<h,int> | ) : total () { a := 10; b := 20; | return (!a + !b); } | | Why is it total instead of st<h>? Won't this have a side effect | of setting the references? | daanx wrote: | Ah, I think Stephen meant to write the following: | fun add-refs( a : ref<h,int>, b : ref<h,int> ) : st<h> int { | a := 10 b := 20 (!a + !b) } | | where indeed the effect is `st<h>` as the updates are | observable. How the function was written before, the two | arguments use a "rank-2" polymorphic type and the heaps are | fully abstract -- in that case it would be unobservable but you | cannot create such values :-) | skybrian wrote: | I expect that, as with any other type system extension, the more | granular your effects are, the more likely you are to run into a | "what color is my function" problem. If you have a public API | that declares certain effects, you're stuck with those unless you | break backward compatibility. | | In a practical system, when writing a library and especially an | abstract interface, you'd want to be careful what you promise and | declare effects that you might need (but currently don't use), | just in case you will need them later. | | It's not that easy even to distinguish functions that can fail | from those that can't, if you're trying to anticipate how a | system will evolve. Something that's in-memory now might change | to being done over the network later. | lambda_obrien wrote: | An effects system like this is more about controlling your own | code and allowing for switching off implementations easily | versus declaring what effects it has. Your declaration of | effects on your function is saying, for example, "I need to | output some text," and then in the caller of that function you | have to do some action to "consume" that effect. For instance, | your example might be an effect called "WriteState" and then | you could call that function in a small unit test with a thin | layer over a Map, in dev you could call it with a local sqlite | db, and in prod you'd call it with your postgres or whatever. | Each implementation shares a common interface, but does | something different with the data. If you were writing a | library, you'd give your public API as the base monad of your | library, or as IO maybe, or even give a pure API. You should be | dealing with the possible failures under that base context and | then the user doesn't need to know about the inner failures. | | The effects system _effectively_ acts as an abstraction for | some side effect, like an interface, and gets ride of a lot of | the boilerplate code needed for mtl or custom transformer | stacks. | | Also, in strict typing it's pretty easy to refactor with modern | linters and such, it actually makes refactoring an API change | delightfully simple, just get rid of the red squiggle lines | telling you you types are wrong. | skybrian wrote: | Refactoring tools are nice so long as you are in a closed- | world environment where you can see all the code and make | whatever changes are needed. They don't help nearly as much | in an open environment where there are many code owners and | not all code is visible to you. When you publish a library, a | refactoring tool isn't going to tell you everyone who uses | your library, and you don't have permission to change the | call sites anyway. | | The only thing for it is to push a new, incompatible version | and other people will have to migrate. So you're pushing off | the work onto them. | | I don't see how an effects system helps with this much? It | might help you better understand how you painted yourself in | a corner, but I don't see how it helps you get out of it. | Quekid5 wrote: | I mean... if you're fixing a broken API, you're going to | have to bite the bullet _either way_ -- papering over it | isn 't going to fix anything... it just hides the problems. | | With a type system which understands effects, at least the | compiler can give you very accurate help in fixing call | sites. | skybrian wrote: | It does help by telling you what's wrong. But in a way, | increased precision makes the problem worse. | | Suppose you have have a language with two categories of | functions, those that can fail (returning an error) and | those that can't. | | It's nice that within the "functions that can fail" | category, you don't have to worry about what kind of | error it might be. Error propagation can happen in a | generic way. Adding a new kind of error doesn't change | any API's. | | If instead, you have different kinds of errors and | _declare_ them everywhere, you end up with a situation | like Java 's checked exceptions. The problem is being | _too precise_ , which doesn't leave room for changes | later. | | Similarly, we could be very generic about effects. Maybe | we could just say "this function has effects" and treat | them all the same? By not being precise about what the | effects might be, we aren't promising too much, so we | allow ourselves room for change. | | The downside is that the caller has to assume any effect | is possible. This is a fundamental tradeoff between | caller and callee convenience. You need to be specific | enough for the caller to be able to deal effectively with | the effects you declare, but not too specific, or you're | painting yourself in a corner. | jahewson wrote: | I always found IOException in Java to suffer from this problem. | AaronFriel wrote: | A properly done effects system with type-level annotation of | the "color" of functions helps this, rather than hurts as you | might surmise. | | Take for example logging or tracing - we almost always in a | modern backend application want an ambient trace or span ID and | a log destination. What we don't want is to have to add those | as parameters to _every function_. | | So we want to paint these functions with the "logger" and | "traced" colors, which respectively allow a function to send | messages to an ambient logger via log(message: str) and to get | the current trace context via getTraceContext() each with no | other arguments. | | The compiler will tell us if we call these functions from an | unlogged, untraced function, so at a very high level - say at | the RPC request level, we paint them early on. In Haskell we | might even be able to do something like | handleReq req = withTraceSpan (...) . | withLogging (...) $ handleReqInner req | | Where handleReqInner requires these "colors". | | Having too many effect handlers is never a problem either - you | can always call an untraced function from a traced function. | The "trace" effect handler just falls off at that call. Or in | other words: you can always call a function whose colors are a | subset of the call-site's. | | This is somewhat a foreign concept to people who haven't worked | with type systems like this and whose only concept of the color | of function is purely binary - either async or not. In a good | typed effect system, the compiler will assist you in knowing | where you're missing an effect handler, and it will be easy to | add those effect handlers sufficiently far from the business | logic that you won't have to think about it. | skybrian wrote: | You're saying it's not a problem, but let's say you didn't | think ahead and want to add tracing later, and there are many | intermediate function calls between top level and the place | where you want to trace. You still have to change every | function signature to add the colors, right? | AaronFriel wrote: | In a language and type system where the compiler infers all | of the intermediate types, no. | | The way effect systems are typically added to languages | without this level of type system is that the effect | "fails". Like making a database call in Python without | "with use_database(...):" that provides the database | context. In those languages and with those effects, | typically the default behavior is the effect's methods are | always available, but may either be a no-op/return a | nullish value or throw an exception. | | As an example, the way tracing works on Node.js is that you | have to explicitly declare a span (or use a plugin for a | framework which does that for you) and inside that span, | you can do something like getSpanId() and it'll return a | value. Outside a span, it returns null. | | The worst case scenario is you have a language and | framework where it's both difficult to annotate the types | and difficult to provide some ambient context. In those | cases, you're back to (essentially) passing your trace | span, your logger, etc as arguments to every function | again. Not ideal. | dan-robertson wrote: | In every language I've used where types can be inferred, | almost everyone writes down the types of their top level | functions (or at least the functions that are exposed to | clients). I don't think this is really a good counter | argument. | | The only ways I know of to reliably make this kind of | change (or just about any nontrivial change) to an | interface are increasing version numbers and letting | clients suffer or having a monorepo and fixing all the | clients yourself | Quekid5 wrote: | Yes, you do... which is as it should be. What else would | you expect? | | If a function needs capability X then it must require that | capability. Simple as that. | shthrow wrote: | No, think of it like checked exceptions, you only need to | handle the effect _at most once_ before it reaches the top | (it's up to you to let it keep getting passed up or handle | it immediately), otherwise the compiler will automatically | infer the effect types in the layers between. | skybrian wrote: | Okay, so type _inference_ is the vital feature here. | | It can help in a closed code base. In an open code base, | you could inadvertently change a public API, breaking | callers that are unknown to you, if the type signature of | something public is inferred. | lalaithion wrote: | The "what color is my function" problem is insurmountable in | Javascript, because the runtime does not allow you to call an | async function from a non-async one. However, most effects | aren't like this. If you say "this function needs randomness" | then you can create a pure PRNG and call the function with the | PRNG providing randomness. If you say "this function needs | logging" you can tell it to log to a string and parse/ignore | the string. If it throws an exception, you can catch the | exception. Haskell even provides `unsafePerformIO`, which lets | you run arbitrary computations as if they were pure. | skybrian wrote: | There is the specific issue with async functions, but that's | only one example of a general problem, what I'm calling | "function coloring." Workarounds are often possible, but they | are still workarounds and often result in bad code. | | We've been there with Java. An API takes a Runnable. You need | to do something that does IO, so you catch the exception... | and then what? Log and suppress it? This is how bad code | happens. At best you get chained exceptions. | | With Callable, they got smarter and declared it to throw | Exception, which means it can only be easily propagated by | other methods that declare they throw Exception. So it's a | viral declaration, unless you engage in workarounds like | exception wrapping. | | You don't need an effect system for this to happen, though. | In Go, you don't have the problem with different kinds of | errors, but you can still classify functions into two | categories: those that may return an error, and those that | (apparently) always succeed. If you need to fix the API, you | might end up with lots of refactoring [1]. | | Mistakes at the API level can be hard to fix without touching | lots of API's. This is unsolvable in general. The only way to | avoid workarounds (via escape hatches or by mocking things | out) is to standardize whatever you can so it works together. | | [1] https://www.dolthub.com/blog/2020-11-16-panics-to-errors/ | lalaithion wrote: | Nothing prevents having a type system capable of dealing | with those problems. Yes, Java and Golang make this | difficult, but if you have a language that supports it, | there's nothing which prevents writing an API that says | "Whatever the effects of the Callable you passed me are, I | also perform those effects". | skybrian wrote: | Introducing generic types doesn't make the function- | coloring problem go away. | | Now, instead of having a single function that has some | effects, you have a family of closely-related functions, | each with different effects. These functions are | incompatible with each other. The function-coloring | problem has gotten worse! :) | | (Generic types are still useful though.) | no_wizard wrote: | I think the reactive/rx/observer patter might be an | acceptable solution around the _what color is my function_ | problem and it tends to be very popular with the functional | programming wing of the JS community from what I've seen. | | Never understood why the browser didn't ship a full access to | something like an EventEmitter. Since you can dispatch events | on window document DOM elements etc seems like being able to | subscribe to events in a more arbitrary fashion that worked | in parallel to the other events would have been useful and | solved callback hell all the same ___________________________________________________________________ (page generated 2020-11-22 23:00 UTC)