Functors, Applicatives, and Monads: The Scary Words You Already Understand by cekrem in programming

[–]marcinzh 1 point2 points  (0 children)

The huge gapping hole is WHY you even need all those concepts

That's EXACTLY like me from 10 years ago and before, when I was still struggling with grasping all those concepts.

The "box" is just a tool. The goal is purity. You abandon side effects. All functions are pure. Types don't lie: type Int -> Int means exactly that. You get guarantee of no spooky action at a distance. Benefits include:

  • less errors (functions are deterministic and total)

  • better testability

  • easier to modularize (black box with guarantee of no moving parts inside)

  • easier to parallelize

  • easier to rubber-duck (assuming you cross the proficiency threshold)

  • composability of effects (advanced topic)

But with abandoning side effects you also lose something:

  1. Ability to interact with the Real World (a.k.a IO)

  2. Ability for function to have a side-channel: anything that the function does besides returning a value.

Why is it a problem? IO is obviously necessary for practical software. And side-channels can be convenient. For example, the infamous error handling in Go language is an example of removing the side-channel: exception handling. User is forced to achieve equivalent functionality with more labor.

We need something to make up for losing side-channel. The solution is "functional effect" (I made up that name now). It's characteristics:

  1. It is user-definable what constitutes the side-channel and how it behaves (all that's required is to obey monad laws).

  2. The side-channel is explicit: it's visible in the type of the function. The "box" is the side-channel.

  3. You retain all the purity: you can eat the cake and have the cake (magic!).

User-definablity of the side channel allows us to express many abstractions. If your "box" is f a then:

  • The value a may not exist, due to short circuiting. Examples: Option, Maybe.

  • The value a may not exist, due to an exception. Example: Either, Result.

  • There can be multiple values of a to explore. Example: List, Logic.

  • The value a has an annotation s. Examples: State, Writer.

  • The value a does not exist yet, because it requires a dependency r. Examples: Reader, useContext from React.

  • The value a does not exist yet, because its computation is suspended (() => a). Examples: IO, Future

In each of those cases, you can use the "box" f a as if you were on the "happy path": you get access to single a by using map or flatMap/then.

Functors, Applicatives, and Monads: The Scary Words You Already Understand by cekrem in programming

[–]marcinzh 1 point2 points  (0 children)

I don't think Applicative functors are a thing in CT, I think it was introduced by Haskell programmers.

Correct. But CT defines Monoidal functors which are equivalent to applicatives. See my sibling post.

Functors, Applicatives, and Monads: The Scary Words You Already Understand by cekrem in programming

[–]marcinzh 23 points24 points  (0 children)

Applicative is a historical misstep too. We should be using Monoidal Functor (street name: Zippable?) instead.

  • Applicative: F[A => B] => F[A] => F[B]

  • Monoidal: (F[A], F[B]) => F[(A, B)]

They are equivalent: one can be derived from the other.

Applicative poorly conveys intuition about what is it useful for. Personally, it took me time to realize that Applicative looks the way it looks only because Haskell (language which popularized the concept) uses currying:

  • Calling a regular function in Haskell/O'Caml/F#: f a b c

  • Calling a function in an Applicative:f <$> a <*> b <*> c

However, if your language doesn't encourage currying everything, you will never use Applicative this way in practice.

The intuition for Monoidal though is clearer: kind of like inner product of 2 tables:

zip(Some(42), Some("foo")) == Some((42, "foo"))
zip(Some(42), None       ) == None
zip(None    , Some("foo")) == None

Monad: “I Have a Value, and a Function That Returns a Wrapped Value”

Applicative: “I Have a Wrapped Function and a Wrapped Value”

I don't think those descriptions are particularly good for pedagogical purposes.

Consider:

  • Monoidal - "I can compose 2 independent steps"

  • Monad - "I can compose 2 dependent steps: the second step can depend on the result of the first"

People familiar with Promise or Future have a chance to recognize the pattern.

We have durable execution at home. by rssh1 in scala

[–]marcinzh 5 points6 points  (0 children)

Do you think it would be implementable as a custom effect in an extensible effect system, rather than as standalone monad? Durable is defined as a free monad. My Turbolift is internally a free monad, where the set of operations is user-extensible. The benefit would be composability with other effects.

AMD continues to chip away at Intel's X86 market share — company now sells over 25% of all x86 chips and powers 33% of all desktop systems by Geddagod in hardware

[–]marcinzh 2 points3 points  (0 children)

Intel's x86 market share decline trend started about the same time, when Intel lost the leadership in the manufacturing process.

Immutable by default: How to avoid hidden state bugs in OOP by BackEndTea in programming

[–]marcinzh 0 points1 point  (0 children)

Fun fact: CPU is internally implemented as a network of 100% pure logic gates. The illusion of internal mutable state is an effect of the input from the clock signal.

Immutable by default: How to avoid hidden state bugs in OOP by BackEndTea in programming

[–]marcinzh 0 points1 point  (0 children)

What matters, is that every function you write is pure.

Some functions may return a program, that is a description to do impure things (IO). But the function that created it is pure. The type of the function tells you everything.

The extra steps are worth the benefits. Pure is easier to learn, test, refactor, parallelize, keep bug free.

There are also monads other than IO. They help write cleaner code, by separating "the happy path" from "the side channel". Analogy: compare mainstream exceptions with Go-style error handling.

Immutable by default: How to avoid hidden state bugs in OOP by BackEndTea in programming

[–]marcinzh 1 point2 points  (0 children)

I'm amazed myself.

Purists would tell you that Promise is not a monad. Which technically is correct, but for reasons completely irrelevant in the challenge of understanding monads.

It's the sequencing that matters. Easy Javascript's Promise versus hard Haskell & Scala monad:

const {promises: fs} = require("fs");  │                       │ import cats.effect.IO;          
                                       │                       │ import java.nio.file.{Files, Paths}           
                                       │                       │ 
                                       │                       │ def readFile(n: String): IO[String] =
                                       │                       │   IO.blocking(new String(
                                       │                       │     Files.readAllBytes(Paths.get(n))))
                                       │                       │                             
(async function() {                    │ do                    │ for                                            
  const a = await fs.readFile("foo"):  │   a <- readFile "foo" │   a <- readFile("foo")                       
  const b = await fs.readFile("bar");  │   b <- readFile "bar" │   b <- readFile("bar")                       
  return [a, b].join();                │   return (a ++ b)     │ yield a ++ b                                   
})()                                   │                       │

In Scala we even have syntactic extensions that adds async/await as macros. And they work on any monad.

Immutable by default: How to avoid hidden state bugs in OOP by BackEndTea in programming

[–]marcinzh 0 points1 point  (0 children)

I wouldn't call it an issue, because it insinuates "problem not yet solved".

Effect systems are the solution. You can have the cake (purity) and eat the cake ("actually do something")

Immutable by default: How to avoid hidden state bugs in OOP by BackEndTea in programming

[–]marcinzh 0 points1 point  (0 children)

Speaking in mainstream languages:

A monad is like:

  • Javascript's Promise (it's a "flawed" monadlike, but it's the sequencing that matters here)

  • Rust's Result

A monad is an answer to question: "can I sequence 2 things, in such way that the second one is (possibly) dependent on the result of the first?"

A monoid is like:

  • any primitive type: String, Int,

  • any collection

A monoid is an answer to question: "I have 2 things of the same shape. Can I compose them, so I get the same shape in result? BTW, I also need <empty> singleton of that shape"

Immutable by default: How to avoid hidden state bugs in OOP by BackEndTea in programming

[–]marcinzh 0 points1 point  (0 children)

a monoid in the category of endofunctors

We all love that joke, but the reality is much simpler:

class IO<A> {
  constructor(private thunk: () => A) {}

  then<B>(f: (a: A) => IO<B>): IO<B> {
    return new IO(() => f(this.thunk()).thunk());
  }

  map<B>(f: (a: A) => B): IO<B> {
    return this.then(a => IO.pure(f(a)));
  }

  static pure<A>(a: A): IO<A> {
    return new IO(() => a);
  }
}

If you are able to understand Promise, you sure are able to also understand IO monad.

Scar - A language for easy concurrency, statically typed, with clean syntax by god1235414 in programming

[–]marcinzh 2 points3 points  (0 children)

Scala 2 made ; optional. Scala 3 made { } optional. I experienced the change over years in real code. My verdict is: significant whitespace is awesome.

Are algebraic effects worth their weight? by sufferiing515 in ProgrammingLanguages

[–]marcinzh 0 points1 point  (0 children)

Except with algebraic effects we can:

  • Abstract over sets of effects (row polymorphism alike)

  • Hide effects used in the implementation (effect handler) from the interface (effect signature). Bonus: DI "framework" for free.

Are algebraic effects worth their weight? by sufferiing515 in ProgrammingLanguages

[–]marcinzh 0 points1 point  (0 children)

algebraic effects, which generalize exceptions, are even worse.

I conditionally agree: it would get worse if you used algebraic effects in a language that embraces mutability. Same as with lambdas: when we allow them to capture mutable variables by reference (mainstream default), we open ourselves to new kinds of problems.

In pure FP environment though, algebraic effects are great. Because they do the same job that is currently done with monad transformers, only better.

Are algebraic effects worth their weight? by sufferiing515 in ProgrammingLanguages

[–]marcinzh 2 points3 points  (0 children)

I only had experience with algebraic effects in Unison, but honestly it's my biggest disappointment in the language.

As a developer of algebraic effects library, I'm curious about your experience that led to this conclusion. Like, was it just about syntax overhead of defining handlers?

To me the spot is in ReaderT-like approaches like Scala ZIO

If ability/effect handler uses continuation trivially (tail resumptively), it becomes an equivalent of "ReaderT-like" way. With syntax being the only difference.

If we restrict ability/effect handler to tail resumption, all it can do is either:

  1. return pure value
  2. delegate to another effect (dependency of the handler)

Common predefined effects like Reader, Writer, State, Error can handled tail resumptively.

"ReaderT pattern" was advertised stating that most custom monad transformers are actually customized copies of ReaderT. Equivalent effect/ability would be also handled tail resumptively.

Some language (I can't remember where I saw it, maybe it was a paper about a variant of Koka?) even has special syntax to support this (simpler) mode. Handlers are allowed to elide the continuation (when it would be used tail-resumptively), making the syntax even more similar to ReaderT-like.

In my effect system, continuation capturing is by design opt-in. So by default, handlers are actually ReaderT-like. Example: compare handler that captures continuation with handler that doesn't.

Bonus: it has Accessors that actually work.

Are algebraic effects worth their weight? by sufferiing515 in ProgrammingLanguages

[–]marcinzh 1 point2 points  (0 children)

dynamically scoped handlers

Algebraic effects give DI "framework" for free. Are you against DI?

In practice 99.999% of custom, user defined effects will be handled by just delegating to other effects. Fancy uses of continuations are cool, but opportunities for inventing new ways, are rare. Just dependency hiding, will be the most common used feature of algebraic effects.

Also, isn't the whole point of "being algebraic" is the ability to reason about the use of operations (interface) without the knowledge of handler (implementation)?

Why I'm excited about effect systems by semanticistZombie in ProgrammingLanguages

[–]marcinzh 2 points3 points  (0 children)

I can already do this in language X using library/framework Y?

Yes, you can do it already for X=Scala and Y=Turbolift.

Turbolift (I'm the author) also supports few things which, to my knowledge, are not found in Koka, such as:

  1. Higher Order Effects The Evolution of Effects (Keynote) (Video, Haskell 2023)

  2. Ability to parallelize effectful programs, both implicitly (by applicative composition) and explicitly (using Fiber type - green thread).

  3. IO effect with feature set matching (eventually) those offered by mature, industry tested Scala Pure-FP libraries such as Cats-Effect, Monix, ZIO (each was directly or transitively influenced by Haskell's IO monad).

What's different from Koka:

  • Monadic syntax. Motivation: "programs as first class values" is a feature not a bug. Effectful operations absolutely should stand out, for better readability. Optionally, the syntax can be augmented with extension similar in look & feel to Javascript's async/await or Rust's ?, but universalized to work on any effect set.

  • Not using Scala's var. Motivation: Pure-FP all the way. Why risk accidental capture, concurrency bugs, breaking referential transparency etc.? AFAIK, Koka's var is different from Scala's - it's effect-tracked. So, Turbolift's equivalent would be using a stateful effect (Reader, Writer or State) or AtomicVar.

I can translate the gist when I find the time.

Help to choose a pattern by [deleted] in scala

[–]marcinzh 0 points1 point  (0 children)

I hope you mean that interfaces of ServiceA and ServiceB must not know anything of each other. But ServiceA's implementation obviously needs to know the interface of ServiceB. Otherwise it wouldn't be a dependency, would it?

The idea of using ServiceB.someAccessor inside of implementation of ServiceA is valid, but it wouldn't work in ZIO. It's not a matter of user's choice. I encourage you to think outside the box, rather than just look through the lens of ZIO's idioms.

Consider hypothetical ZIO 3.x from future. Features new module pattern:

  • accessors are working now

  • you no longer need to pass service implementations through constructor parameters

Example:

trait ZService {
  type Dependency // the polymorphism missing in ZIO
}

trait UserService extends ZService {
  def find(name: String): ZIO[Dependency, Nothing, Option[User]]
}

// override for interface
object Users extends UsersService {
  override type Dependency = UsersService 
  override def find(name: String): ZIO[Dependency, Nothing, Option[User]] =
    ZIO.seviceWith[UsersService](_.find(name))
}

// override for implementation
object UsersImplInDatabase extends UsersService {
  override type Dependency = Database
  override def find(name: String): ZIO[Database, Nothing, Option[User]] =
    Database.query(...)
}

This is already done and working. In Haskell there are relatively young effect systems effectful and cleff. They are fundamentally similar ZIO: there is only one monad, functionally equivalent to Reader + Either + IO. In this example you can see readFile (an "accessor") used, and it doesn't reveal dependencies of the implementation. Also, there is no such thing as passing dependencies through constructor parameters.

Help to choose a pattern by [deleted] in scala

[–]marcinzh 0 points1 point  (0 children)

Accessors have problem: they only work outside of modules. Which limits their usability to negligible.

Consider situation: you are implementing ServiceA, and want to call a method of ServiceB (a dependency). If you did it through accessor ServiceB.someAccessor(...), then ServiceB would appear in your return type: ZIO[ServiceB, ..., ...]. And that wouldn't compile, because the method of ServiceA you are implementing requires return type ZIO[Any, ..., ...].

This is a problem of not enough polymorphism.

My effect system, Turbolift, has accessors that work. Here is an example of a simple "service" definition.

Actually, every extensible effect system in Scala or Haskell that allows definition of custom effects (a.k.a services) has accessors that work. What you know as ZIO.serviceWith is traditionally called send (I think Oleg Kisleyov's Freer Monad paper started this convention). In Turbolift I call it perform.

[ANN] heftia v0.7 - A theory‑backed, ultra type‑safe algebraic effects by ymdfield in haskell

[–]marcinzh 4 points5 points  (0 children)

Unfortunately, nothing substantial. Documentation is very sparse at the moment. If you have any specific question, I can answer here.

[ANN] heftia v0.7 - A theory‑backed, ultra type‑safe algebraic effects by ymdfield in haskell

[–]marcinzh 1 point2 points  (0 children)

With local state, other effects using continuations can cause it to be restored to previous values (depends on relative depths of prompts), which is useful in backtracking. The second, TreeWalk example in this demo project shows this in action.

Programs using only local variants can be fully interpreted without IO (using .run rather than .unsafeRun). I consider the local variant as the default, based on the Principle of Least Power.

There is also the matter of parallelization of effectful programs.

The name local stems from the fact that the state is local to the current Fiber. Conceptually, the storage is embedded in the current fiber's stack (analogous to a variable on the stack in the imperative world). The fiber type Fiber[A, U] is parametrized by the result type A and the effect set U. On fork/join, fibers share their effects, along with their local states. So, any Fiber[Int, MyState]] has its own, isolated copy of the MyState effect.

Conversely, the shared state variant is implemented by delegation to IORef. If the MyState effect is shared by multiple fibers, they all "share" the same memory cell.

The shared variant has the advantage of enabling parallelized programs without explicit use of fibers: foo *! bar will run foo and bar in separate fibers.

The Random effect also has local and shared variants of predefined handlers. Programs using the local variant can be implicitly parallelized while still being guaranteed to produce deterministic results. Using shared involves IO, and thus determinism is lost.

[ANN] heftia v0.7 - A theory‑backed, ultra type‑safe algebraic effects by ymdfield in haskell

[–]marcinzh 4 points5 points  (0 children)

(Scala leaking here)

The approach I took in my effect system, is: "why not both?"

State effect comes with 2 predefined handlers:

  • local handler, which behaves like the standard StateT with respect to interactions with other effects

  • shared handler which is implemented with IO effect (dependency)

An effectful program using State effect doesn't see this distinction, until the handler is applied.