linear-base
linear-base copied to clipboard
Don't export `fst` (and alike) from `Prelude.Linear`
It is surprising that Prelude.Linear
exports the non-linear version of projection fst :: (a,b) -> a
.
This tripped me up (with non-inspiring errors about multiplicity) when writing evalState
:
- #404
I was naturally expecting that fst :: Consumable b => (a,b) -> a
would be by default in scope if a fst
is in scope. After all, we want linear programs when we import Prelude.Linear
!
So, I suggest either to not export fst
(and alike) or export them from Data.Tuple.Linear
.
There is a reason why we export these versions of fst
and snd
(not that we didn't have oversights of the sort (#354)).
The reason is that it's consistent with the behaviour of record types:
data Pair a b = MkPair { fst :: a, snd :: b }
We have:
MkPair :: a %1 -> b %1 -> Pair a b
fst :: Pair a b -> a
snd :: Pair a b -> b
This is the only type that we can give to fst
and snd
; unless we have an affine multiplicity, or GHC knows of Consumable
and we give the type that you suggest. One problem with the latter approach is that it's not backward compatible with regular Haskell.
I should say, more generally, that I'm always rather suspicious of functions with Consumable
constraints in their types. They sometimes make sense, but it's worth remembering that consume
can sometimes be pretty costly (it usually requires traversing linearly the data it's consuming; but since this data has thunks, it may hide arbitrary computations, which you may not expect when writing fst x
).
After reading this what are your thoughts?
I agree that for arbitrary records, linear projects would be awkward (needing one Consumable
constraint for each field that is thrown away by the projection).
For just pairs, it does not look so terrible, but I can accept your reservations about Consumable
.
So, the remaining option would be to not export fst
from Prelude.Linear
so the user has to consciously import it from either Data.Tuple
or Data.Tuple.Linear
. Or, have a Prelude.NonLinear
that exports non-linear fst
and the like, to avoid the confusion that the non-linear fst
is imported via (seemingly) linear Prelude
.
I think what deepened my confusion is that I misread the index (https://hackage.haskell.org/package/linear-base-0.2.0/docs/doc-index-F.html), which lists fst
as:
1 (Function) Prelude.Linear 2 (Function) Data.Tuple.Linear
I was somehow assuming that this is the same function exported twice, but I should have looked more carefully to recognize that these are different functions (nice if the index gave the type signatures...).
Not sure what to recommend. It might just be a beginner's learning experience. Maybe do nothing for now and wait to see if more people suffer from the same confusion as me.
Hey, I'm back :slightly_smiling_face:
Looking at it with fresher eyes, I think that the mistake is using the names from base
for
fst :: Consumable b => (a, b) %1 -> a
As it can be argued that it doesn't have the expected behaviour. Ironically it could be called consumeSnd
, or something like this… But maybe there could be a good name, with a naming convention that we can reuse for this sort of things. Any idea?
I think my expectation was that this is just fst
.
Disambiguation by qualified imports wouldn't be so terrible if one had a simple way to import the linear versions of Prelude
functions in one go. E.g.
import Prelude as NonLinear
-- OR: import Prelude.NonLinear as NonLinear / as Ur / as U
import qualified Prelude.Linear as Linear / as L
...
... fst ...
... NonLinear.fst ... (Ur.fst) (U.fst)
... Linear.fst ... (L.fst)
However, atm Prelude.Linear
is something else than providing linear versions...
For whatever it's worth, I have a slight preference for consumeSnd
: this function will perform something on the second argument and I think it's nice for the name to reflect that.
For whatever it's worth, I have a slight preference for
consumeSnd
: this function will perform something on the second argument and I think it's nice for the name to reflect that.
I agree. However, my fear is that it's not really generalisable: how would we name the list-zipping functions, then? zipAndConsumeLeftover
? Doesn't sound super nice.
Similarly, would fst3
then be named consumeSndAndThd3
?