hutao
Joined 11 months ago
Joined 11 months ago
However, the continuation monad has limitations. In your example, it serves a similar purpose to an abstract monad "typeclass"; where you can write an abstract "task" function and run it in the Result effect, or the Promise effect, or any other effect. However, this "task" function can only use map/bind/return, and cannot perform any specific "effectful" operation, such as throwing an error or performing async IO. At the point in the code where it performs an operation, it has to pick a concrete type for the continuation result type `t`. So, a function that performs async IO would need to return `K(Promise(b), a)`, and a function that throws an error would need to return `K(Result(b, Nil), a)`. What would be really useful would be to have a function can perform an effectful operation (such as throwing an error) and still return something like `M(b)` for some abstract effect `M`. This is one of the main problems of "effect systems," and to my understanding, what algebraic effects are currently trying to solve.
ML also has an interesting module system, which achieves similar goals as object-oriented programming, but in a very different way. While mainstream "class-based" OOP identifies the data type with the unit of encapsulation, ML has "modules," which are separate entities from types, and which may both define types and values as members. However, a downside of this approach is that you sacrifice late binding, as the module language and "core" language are stratified into different layers. Instead, dependency injection is achieved through module-level functions, called functors,
A lesser-known fact is that ML originated as the scripting language for the LCF proof assistant (which is where the name "ML," or meta language, comes from). In the LCF tradition, the inferences rules of the logic are implemented in a trusted kernel, then clients use those inference rules to write tactic scripts that construct theorem objects. This design was supported by ML's module system: theorems were defined as an abstract type in a "kernel" module, and the inference rules were defined as functions inside the module. Here is a tutorial that explains the LCF design better than I can: https://www.cs.cmu.edu/~fp/courses/15317-f17/recitations/rec...
- An algebraic theory consists of a "signature" (specifying a set of operation symbols with their arities) and a set of equations about those operations. An algebraic theory is purely syntactic, and does not give meaning to the operations or equations.
- An interpretation of an algebraic theory maps each operation symbol to a concrete mathematical object. It maps syntax to semantics.
- A model is an interpretation in which the algebraic theory's equations hold.
- A free model of an algebraic theory, generated by a set, is basically a model that can be mapped to any other model of the theory generated by the set.
So, an example of an algebraic theory is a group (as in the algebraic structure itself, not the various instances of a group). Its signature consists of the binary operation of combining, the unary operation of inverse, and the nullary operation of identity. The models of a group are its instances, such as the integers under additions and subtraction, or permutation functions on a set. The integers are the free group (that is, the free model of the group) generated by the set {1}.
Algebraic theories can also describe the effects of a programming language (e.g. reader, writer, or state). The free model of an algebraic effect is given by the syntax trees describing its effectful computations. Effect handlers are maps from syntax trees, transforming computations into other computations within the programming language.
> On the other hand I think it would mean that things like Future and general IO wouldn't be algebraic effects.
For what it's worth, Example 2.3 in the paper states that IO is an algebraic effect, albeit one with no equations. What might be confusing is that the handler of the IO effect cannot have the level of concreteness to actually implement the IO operations. Effect handlers only map computations to computations within the "pure" programming language. From the paper:
> What we still lack is a mathematical model of computational effects at the level of the external environment in which the program runs. There is always a barrier between the program and its external environment, be it a virtual machine, the operating system, or the underlying hardware. The actual computational effects cross the barrier, and cannot be modeled as handlers. A handler gets access to the continuation, but when a real computational effects happens, the continuation is not available. If it were, then after having launched missiles, the program could change its mind, restart the continuation, and establish world peace.
Contrary to the claim from the comment you are replying to, according to Andrej Bauer, "algebraic" does not refer to the composition of different algebraic effects via composing their handlers.
When you see "algebra," think "algebraic structure," i.e., a set of operations and a set of equational laws on those operations.
An algebraic effect consists of a set of effectful operations. In that sense, each algebraic effect (reader, state, etc.) defines its own algebraic structure. In theory, the operations of an effect should also be related to each other by laws. Here is an example for the state effect from Andrej Bauer's paper:
lookup(ℓ,λs.lookup(ℓ,λt.κst)) = lookup(ℓ,λs.κss)
lookup(ℓ,λs.update((ℓ,s),κ)) = κ()
update((ℓ,s),λ_.lookup(ℓ,κ)) = update((ℓ,s),λ_.κs)
update((ℓ,s),λ_.update((ℓ,t),κ)) = update((ℓ,t),κ)
Therefore, the state effect has an algebraic structure.However, monads that cannot be defined in this equational style cannot be translated to algebraic effects. An example is the continuation monad: https://old.reddit.com/r/haskell/comments/44q2xr/is_it_possi... (I think you are involved in the Reddit comment chain that I'm linking to...)
AFAIK, another example is the "exception" monad, because the way that you interact with it is through the handler itself. I once saw a thread on r/Haskell discussing this, but can't find it.
Stack Overflow was the "flagship" product of the Stack Exchange company, and if the company pivots to AI, I wonder what the future holds for the other Q&A sites on the SE network.
I do think "There are no function colors in Go in the way being discussed," versus "all functions [in Go] are red" are two slightly different ways of formulating the same set of facts, and the distinction between them is insightful, so that was what I wanted to touch upon. Namely, I wanted to point out that there is an "implicit" color within the programming language itself.
Async/await is one implementation of cooperative concurrency, where the programmer must explicitly annotate the points where a context switch may occur. However, one can imagine a program transformation that marks every function as async, and makes every function call an await. After making that transformation, the async/await annotations would no longer be necessary. The end result is pre-emptive concurrency, where the runtime may potentially interrupt the active thread at any function call.
To make another analogy, Haskell requires all IO actions to run in the explicit IO monad, while most languages (C, Java, JavaScript, etc.) do not distinguish between "pure" and "impure" functions. Therefore, C, Java, and JavaScript could all be said to implicitly run in the IO monad.
Async IO is also an instance of a monad. In JavaScript, all async functions must run inside the explicit async IO monad, while Go does not distinguish between async and sync functions. Therefore, Go implicitly runs in the async IO monad. This is similar to the aforementioned distinction between cooperative (made explicit to the programmer) and pre-emptive (handled implicitly by the runtime) concurrency.
In fact, Eugenio Moggi, the PL theorist who realized monads could describe programming languages, was not looking for a programmer-facing abstraction. Rather, he was trying to describe the "implicit" monad in a programming language's semantics (such as the IO monad in most programming languages, or the async IO monad in Go).
> By contrast, in a function coloring situation, if the color is wrong 10 layers down, you must change the calling function. It's a non-local consideration. You don't get to decide not to change it. You can't encapsulate it. You don't get a choice. It pollutes the entire stack, forcibly.
I think this is an interesting perspective, where I would raise a counterpoint. Both result types and async/await are instances of monads (the abstraction which approximates the article's idea of a function color, since you mentioned Haskell, I assume you know this). Just as you can "eliminate" the result type by explicitly handling the success and error cases, you could, theoretically, "eliminate" the async function by blocking on it. Doing so would treat the entire async subprogram, at the top-level function boundary, as synchronous IO, while the async subprogram would still benefit from concurrency internal to the function.
Compare Example #1:
int topLevel() {
return match fallibleSubprogram() {
Ok(()) => 0,
Err(_) => 255,
};
}
Result<(), Err> fallibleSubprogram() {
let x = f()?;
let y = g()?;
return h(x, y);
}
Compare Example #2: int topLevel() {
block_on(asyncSubprogram);
return 0;
}
async void asyncSubprogram() {
let promiseX = f();
let promiseY = g();
let [x, y] = await Promise.all([promiseX, promiseY]);
return await h(x, y);
}
In the above pseudo-code, you have the same program "structure," but the first uses results and the second uses promises. In the latter example, asyncSubprogram() gets called as if it were synchronous, but you still benefit from asynchronicity because f() and g() can execute concurrently within its body.The main difference is that compared to pattern matching on Result types, programming languages typically make it unidiomatic to block on a promise. There are various reasons why this is the case, but my point is that Result types and async/await are more similar than they may initially appear.
Then, ML grew into a general-purpose programming language (both OCaml and Standard ML are dialects).
There is one situation, however, where Standard ML prefers currying: higher-order functions. To take one example, the type signature of `map` (for mapping over lists) is `val map : ('a -> 'b) -> 'a list -> 'b list`. Because the signature is given in this way, one can "stage" the higher-order function argument and represent the function "increment all elements in the list" as `map (fn n => n + 1)`.
That being said, because of the value restriction [0], currying is less powerful because variables defined using partial application cannot be used polymorphically.
@dang I think the account that I'm replying to might be a bot?
The article mixes together two distinct points in a rather muddled way. The first is a standard "premature optimization is the root of all evil" message, reminding us to profile the code before optimizing. The second is a reminder that async functions compile down to a state machine, so the optimization reasoning for sync functions don't apply.
[0] https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-a...
Your intention of having the restricted domain "NonNegativeLessThanHalfMaxValue" is probably so that both addends have the same domain. If you go down that route, perhaps you'll also want the closure property, meaning that the range should also belong to the same set. However, then you have to deal with the overflow problem all over again...
The real point is that when adding two N-bit integers, the range must be N+1 bits, because the "carry bit" is also part of the output. I think this is a scenario where "Parse, Don't Validate" can't easily help, because the validity of the addition is intrinsically a function of both inputs together.
EDIT: Parent comment was edited to amend the "impossible/unrepresentable" wording
For a concrete example, suppose that you want to define a data type for JSON objects. You would likely need to choose a type to represent JSON arrays (such as `ArrayList`, in Java), as well as a type to represent JSON objects (such as `HashMap`). However, what if the client wanted to use `TreeMap` instead of `HashMap`?
With "kinds," you would be able to define the JSON data type as something abstract like `JSON<ArrayType, MapType>`, and allow the client to instantiate it as `JSON<ArrayList, HashMap>` or `JSON<ArrayList, TreeMap>`. Unfortunately, in a language such as Java, this is not possible, because `ArrayList`, `HashMap`, and `TreeMap` are themselves generic types, while only non-generic types (such as `Integer`, `String`, or `HashMap<String, Integer>`) can be type parameters. However, in a language with higher-kinded types, such as Scala, this becomes possible.
In these higher-kinded languages, a type such as `Integer` or `HashMap<String, Integer>` would have a "kind" of `*`, while a generic type such as `ArrayList` would have a kind of `* -> *` (meaning: a generic type of one, non-generic type parameter), and `HashMap` and `TreeMap` would both have a "kind" of `* -> * -> *` (meaning: a generic type of two, non-generic type parameters).
C++'s templates permit a similar abstraction to higher-kinded types, and the library nlohmann::json (https://json.nlohmann.me/api/basic_json/) in fact does something very similar to the JSON example that I describe. Namely, it uses `std::vector` as the default for `ArrayType` and `std::map` as the default for `MapType`, but also allows the client to parameterize the underlying container of choice. However, C++ templates need to be fully expanded in order to be typechecked, leading to compile-time error messages that can be long and difficult to read. A "kind" system basically solves this issue by imposing a proper type system onto templates, allowing for more understandable error messages.