linear-base icon indicating copy to clipboard operation
linear-base copied to clipboard

Don't export `fst` (and alike) from `Prelude.Linear`

Open andreasabel opened this issue 2 years ago • 7 comments

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.

andreasabel avatar Apr 10 '22 19:04 andreasabel

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?

aspiwack avatar Apr 11 '22 07:04 aspiwack

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.

andreasabel avatar Apr 11 '22 10:04 andreasabel

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?

aspiwack avatar Apr 28 '22 09:04 aspiwack

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...

andreasabel avatar Apr 28 '22 09:04 andreasabel

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.

b-mehta avatar Apr 28 '22 12:04 b-mehta

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.

aspiwack avatar Apr 28 '22 12:04 aspiwack

Similarly, would fst3 then be named consumeSndAndThd3?

andreasabel avatar Apr 28 '22 14:04 andreasabel