Use untagged `IO` as default, with tagged `Eff` to supplement.
Closed this issue Β· 36 comments
Discuss.
We have an IO type in purescript-io. I think that it's something that should be moved into core, and we should encourage new libraries to write IO FFI imports instead, optionally supplementing them with Eff effect labels.
See #20, although I think we should do it the other way around as a breaking change.
I heartily agree with this idea.
Eff's effects are described by strings (well, symbol/kind pairs), and are not semantic. One string in one library doesn't correspond to another string in another library (or sometimes corresponds to multiple in some chaotic and changing fashion), and there is no way to do algebraic effect introduction and elimination, which are cornerstones of a principled algebraic effect system.
The row types in PureScript have been proven to be extremely useful, but I'd argue the effect system that piggybacks on row types has created enormous work for developers and adds layers of complexity not found in Elm, for no more actual guarantee of compile time benefits.
An IO type would make PureScript dramatically simpler, more accessible, and easier to use, while not sacrificing anything significant (strings in row types are still strings!).
I know Bodil and several others have a similar opinion here.
Regarding naming, some comments from elsewhere.
I don't know much of the background on this, but
Eff e a->Eff aseems like a much better way to go if everything currently usingEffandAffshould be removing its effect rows.IOseems better if there's still a use case for keeping the currentEffimplementation around (no idea what that would mean forAff). I'd prefer modifyingEffandAffin that case, because it basically keeps things working the way they are, just easier to work with. Converting toIOseems confusing from a dependency perspective (ex: pulling in 3 libraries and each use a different version of IO/Eff).
me:
I'm slightly more in favour of keeping the
Effname and dropping the row than renaming toIOtoo. I could get used to it either way, but it definitely seems like the update overhead will be lower by continuing withEff/Aff.
I personally am not sure that we should prioritise update overhead vs what name would make the most sense, especially since we are still pre-1.0. Given that (afaict) the name Eff comes from the Haskell extensible effects paper, there doesnβt seem to be any reason for the type to retain the name Eff after we have removed the row type parameter. This change is going to break everything using Eff anyway, so why not choose the name that makes the most sense (which is of course IO)?
The Eff we have is also nothing like the extensible effects paper though, purescript-run is more like that.
The name Eff still makes sense to me as it's "the monad in which effects are performed" regardless of whether its indexed by types-of-effects or not. But yeah, I don't feel that strongly about it.
If we do want to switch to the IO name, I'll see what @jdegoes thinks about donating the purescript-io name for that instead of having it as the current Aff-based IO.
Of course our Eff is quite different to the paperβs, but my impression was always that our Eff was called that because of the characteristic it shares with the paperβs Eff, that there are two type parameters: the first being some kind of set of effects, and the second being the concrete result type. Our current Eff at least has this in common with the paperβs; I think after we remove the row the connection would be much more tenuous.
Generally weβve taken names from Haskell wherever it made sense too, eg βPreludeβ, the Functor hierarchy, <> for the monoid operation, etc, and so I think it would make sense to use the same scheme here.
If the PureScript type had never had a row type parameter, what do you think it would have been called?
If the PureScript type had never had a row type parameter, what do you think it would have been called?
Fair point!
I'm fine donating the name if that's how things go down. The name Eff or Effect isn't that bad, if you consider it from the perspective of Javascript developers exploring PureScript rather than Haskell developers crossing over. But debates over names are not that productive, IMO, so I don't really care one way or another.
I like the idea of using IO since it's common terminology with Haskell, but I also like the idea of keeping the name Eff. However, I think it might be confusing if people find old documentation referring to Eff talking about rows, which then doesn't make sense any more.
One concern I have about IO is that our IO would not be like the one in Haskell in some sense. IO in Haskell is asynchronous, more like the current purescript-io, but our IO would be synchronous. In that sense, Eff makes more sense to me, and I actually think keeping the current purescript-io as it is makes quite a bit of sense.
I'd also consider some other alternatives. A few ideas: Effect,Sync, Task, Method, Proc, Action. I don't feel particularly enthusiastic about any of them, apart from Effect.
Oh, I was thinking that Haskell's IO is closer to our Eff than our Aff / IO but on second thoughts I suppose that's not really the case at all!
I do think it would be good to choose names for the default types for synchronous and asynchronous native effects so that it's clear that they are closely related. So I suppose if we're leaving IO as it is, that suggests we should use consider IOSync or something like that as a new name for Eff?
Also, does it make sense to have two distinct types for sync and async effects in other backends? Should we maybe be considering alternative backends in this discussion too?
So I suppose if we're leaving IO as it is, that suggests we should use consider IOSync or something like that as a new name for Eff?
If we were to go that route, I'd like to call it Sync, but I would like to try to make it clear that Eff is the more basic effect monad in some sense, corresponding to the way in which JS code executes.
Wait, sorry, I mean if IO is the default async effect monad of kind Type -> Type, then I'm just saying it would be best to include "IO" somewhere in the name for the default sync effect monad of the same kind. Why would we have both Sync and Eff?
No, I'm saying I would rather not have IO and IOSync, because that makes it seem like IOSync is a special case of IO, when IO would be built on top of IOSync.
My vote is for renaming Eff :: # Effect -> Type -> Type to Effect :: Type -> Type, and then dealing with Aff and IO separately.
To clarify, I think this is the best option because it a) doesn't lead to confusion with the current Eff, b) doesn't lead to confusion with Haskell's IO, which is asynchronous, and c) is a decent name based on what Eff actually is, i.e. a representation for some side effect to be performed.
Ah, I see. Yes, that sounds good to me, although I think it would be good to have a plan for Aff and IO before we go ahead with this.
Affect? π
Just joking, and in any case I'd like to leave that decision up to the Aff maintainers, and not worry about Aff when choosing the new name for Eff.
I was thinking, I suppose Aff might not need to continue to exist at all?
How's that?
Oh, because it would become IO?
Yeah, exactly. It sounds like we aren't intending to continue providing a type of kind # Effect -> Type -> Type for synchronous effects, so in that case why provide an async version of the same thing, especially since IO already exists?
@hdgarrood I imagine we'll do the same thing with Aff and just drop the effect row. And then rename to Affect π
(or AsyncEffect...)
Whatever happens with the non-extensible effects and naming, I don't really care. But, I think dropping the rows loses something.
It was mentioned in slack that Run a b is closer to the spirit of the paper than Eff a b we have now. In an effort to keep Eff a b with extensible effects, could we bring in Run a b and rename it to Eff a b? The non-extensible effects can still be the default, and the name can be whatever. But, the extensible part can also stay extensible (as the non-default).
What does that achieve over leaving Run as is?
My thoughts:
-
I don't care a lot about the names, but:
-
I like the name
IO -
I like the names
SyncandAsync -
I don't like the names
EffandAff -
I think the name of the sync monad should be related to the name of the async monad (currently they are
EffandAff, respectively) -
So my vote is in favor of
SyncandAsync(orIOandIOAsync, orIOSyncandIOAsync) -
I'd also be fine with getting rid of
Effand just usingAff(renamed toIO) for everything, but that does have a performance penalty
Another 2p from me: I think SyncIO / AsyncIO reads better and is easier to say aloud than IOSync / IOAsync, if we're considering naming like that.
I'm not really a fan of Sync / Async alone, as that name suggests nothing about effects.
I'd also be fine with getting rid of Eff and just using Aff (renamed to IO) for everything, but that does have a performance penalty
and more FFI complexity. I think we need Eff for a simple, sensible FFI.
Also, I think run is an amazing library, but I don't know if it's the best default for the core libraries. The core libraries should contain the simple, low-level abstractions IMO. For the same reason, I like that aff is a separate thing.
Maybe not related but I do really like standardizing the sync and async effect FFI. (Sync/Eff as a simple thunk and Async/Aff as (resolve, reject) => { try { resolve(...) } catch (e) { reject(e) } }. Aff currently supports a lot more than that and that's fine, but that simple FFI integration point that doesn't break between Aff releases is important (since it can't be type checked). Maybe that just means EffFFI and AffFFI should just be compiler built-ins and real libraries should build off of runSyncIOFn and runAsyncIOFn (again, just making up names).
If Eff e a gets renamed to Eff a, which I have come around to preferring because it suggests the default in libraries should be to drop the effect row, i.e. it's part of migration, or if it gets renamed to Sync a (emphasizing more its synchronous nature rather than its effectful nature), then I think we might swap Aff e a and IO a, i.e. IO becomes the new Aff e a implementation (4.0), while Aff e a becomes a smaller wrapper on top for tracking effects via rows. Existing Aff is a good candidate for the IO name since it's asynchronous and supports the moral equivalent of things like forkIO. But it's too complex / opinionated to be a replacement for Eff (though the current version, note, is quite fast for synchronous code, about as fast as `Eff).
If it's that fast it might be better in the long run to use IO for both, then just convert FFI to IO using specific functions similar to the runEffFnX functions. π
If it's that fast it might be better in the long run to use IO for both, then just convert FFI to IO using specific functions similar to the runEffFnX functions.
The main issue with this is that Aff is not guaranteed to be synchronous. It preserves synchronous effects (i.e. does not change them to asynchronous), but Aff a may be some arbitrary combination of synchronous and asynchronous code. Lots of Javascript FFI code requires synchronous execution (e.g. cancel event bubble), so Eff is useful to precisely describe that.
From https://github.com/purescript/purescript-eff#purescript-eff
The Eff monad, for handling native side effects.
I propose Eff e a -> Native a
@gabejohnson I like it, but what about Aff?
@Pauan Aff doesn't have the same baggage as Eff. I don't think there's any reason to change it.
Just a comment to a gradual change: please make it a breaking change (switch to new Eff without rows) and do not mix in the exisiting one. Might be much harder but keeps the clarity of purescript (and the ease of anticipation), which is what everybody likes on the language.
Closing this, since https://github.com/purescript/purescript-effect