-
Notifications
You must be signed in to change notification settings - Fork 0
Extend, Comonad, and StateT #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: last-reviewed
Are you sure you want to change the base?
Conversation
The Setup.lhs file is incompatible with newer versions of the Cabal library. This change adds the appropriate custom-setup stanza to indicate compatibility with Cabal 1.24.
Add custom-setup stanza
by completing the custom setup stanza
Fix new-build on NetworkServer
Test with GHC 8.4
I've had a few people ask for this function in later modules. I also think it's a good exercise before getting to foldRight.
Update tasty pattern examples to match tasty >= 1.0
StateT: Fix typo
Make gitignore ignore cabal artifacts in any subdirectory
Update Leksah instructions.
Remove extra argument of property for ListZipper.index
Add Course.Optional#optional
add editorconfig
Fix typo in Course.Optional#optional
Fix typo in stringTok comment
Fix error typo in Course.MoreParser#string
Add hint about chr to MoreParser.hs
Add a hint to replicateA
Move list and list1 below digit, since digit is used in their tests
The comment that precedes the Traversable type class definition declares there are two laws, but then goes on to define three (naturality, identity, composition). Typeclassopedia does suggest that identity and composition are the traversable laws and that naturality follows if you are dealing with applicatives. But I definitely don't know enough to assess the truth of that claim. (ref: https://wiki.haskell.org/Typeclassopedia#Laws_7) Either way the current comment seems misleading.
Fix typo ('two'=>'three') in Traversable laws
This commit adds some tasty tests for the traversable module. Property-based tests would probably be a good idea and should perhaps be added at a later date. The Compose, Product, and Coproduct newtypes now derive Show and Eq to allow for easier testing.
Add tests for Traversable.hs
ignoring cabal new-test artifact
remove trailing spaces
StateT: add signatures
four Applicative laws
| (<$$>) = | ||
| error "todo: Course.Comonad#(<$>)" | ||
| func <$$> fa = func . copure <<= fa | ||
| -- REVIEW: would it be desireable to eta-reduce this to `(<$$>) f = (f . copure <<=)` ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I think the way it is now is cleaner.
| Functor (Compose f g) where | ||
| (<$>) = | ||
| error "todo: Course.Compose (<$>)#instance (Compose f g)" | ||
| -- (a -> b) -> Compose f g a -> Compose f g b |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: it is possible to add explicit type signatures to instance members with InstanceSigs extension
| error "todo: Course.Extend (<<=)#instance List" | ||
| _ <<= Nil = Nil | ||
| func <<= l@(_ :. rest) = func l :. (func <<= rest) | ||
| -- REVIEW: is this the "right way" to do this, or should I be using foldr or something? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think recursion is perfectly fine in a vacuum.
However, in some cases, it makes the code look needlessly complex, and in these cases you'd use fold - i.e. foldl (+) xs is way more concise than a recursive analog.
But this is not one of those cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When approaching this problem, would you have reached for recursion, or some other looping function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have gone with recursion
| Empty -> Empty | ||
| -- func <<= fa@(Full _) = Full $ func fa | ||
| -- _ <<= Empty = Empty | ||
| -- REVIEW: both of these work, any reason to prefer one over the other? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason other than aesthetics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have a preference? If so, why? I don't have a strong sense of aesthetics yet, so I'm trying to build up my heuristics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a preference for the uncommented way, because I find the @-aliasing ugly.
| pure $ func <$> a | ||
| -- REVIEW: | ||
| -- Above, you suggested solutions like these over more point-free ones like theirs | ||
| -- (OptionalT f <*> OptionalT a = OptionalT (lift2 (<*>) f a)). Does this fall into the same |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think in this case their solution is cleaner: it straightforwardly states that <*> on OptionalT is <*> on Optional lifted through the underlying functor.
Note, however, that their solution is not point-free: it has two points, f and a.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I don't have a good intuitive sense of what lift2 means, and my understanding of what lift means might be muddling it further. My understanding is that lift takes a value that is under one monad in a transformer stack and makes it compatible with the entire stack. Does lift2 mean something similar, or is it completely different?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a (yet another) slightly confusing naming thing: lift2 doesn't actually have anything to do with lift or with monad transformers. lift2 is just an fmap of two arguments, that's it.
To disambiguate this (or, perhaps, to confuse you even more :-), in the standard library it's actually called liftA2, where the A refers to Applicative.
I've got a great read for you on the subject of lifting: https://fsharpforfunandprofit.com/posts/elevated-world/
| -- category? I'm still trying to build my intuition around when it makes sense to go point-free | ||
| -- and when it doesn't. | ||
| -- | ||
| -- Also, their version fails tests 5-7 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has to do with how lift2 is defined for lists. In order for these tests to pass, it has to do what your code above does: first run the first argument, then the second, not both at once. I'm not sure what your implementation here is, so can't say more.
In general, however, it is kind of well known that lists are good as functors, but very weird as applicatives and monads. This is because there are multiple ways of implementing <*> and =<< for lists, and none of them make intuitive sense in all edge cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, scratch that. There is no way to implement lift2 on lists in a way that will accommodate these tests.
lift2 (+) [1,2] [10, 20] == [11, 12, 21, 22]
lift2 ($) [(+1), (+2)] [10, 20] == [11, 12, 21, 22]
It can't know that it has to skip application for some elements, unless it knows the inner structure of those elements, so for lift2 (<*>) [Empty, Empty] [Full 1, Full 2] it will always apply both Emptyies to both Fulls, returning a list of 4.
In general you can say that:
∀f, l1, l2 : length (lift2 f l1 l2) == length f1 * length f2
There is, of course, another way to implement lift2 on lists:
lift2 (+) [1,2] [10,20] == [11, 22]
But that will also not work with some of the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it fair to say that one should (almost) never see lists as applicatives or monads in production code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No.
Even though lists are not well-behaved monads, it is nevertheless very convenient to use the do notation for them:
cartesianProduct :: [a] -> [b] -> [(a, b)]
cartesianProduct ax bx = do
a <- ax
b <- bx
pure (a, b)
Of course, in this specific case I could use a list comprehension, but there are more complicated cases where list comprehension comes out really ugly, and monadic syntax looks very understandable. And people do this all the time. In fact, PureScript doesn't even have list (array?) comprehensions, and instead recommends using the monadic syntax.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So then what's the right way to think about this problem?
- It's aesthetically more pleasing to use
lift2, but doing so doesn't meet the necessary requirements, so we have to use a slightly uglier solution - The specs are defined wrong in some way, it should be possible to use
lift2to solve this problem - Something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's an approximation thing. They behave well enough in mainstream cases. It's only edge cases that are weird.
It's like Newton's gravity is not strictly speaking correct, so you should ideally use relativity, but it is correct enough under normal circumstances that you can still get away with it most of the time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do I know when I'm getting near the edges of the safe uses of list monads/applicatives? Or is that just from experience?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends on what you mean by "safe".
Technically, everything always works exactly the way it was programmed to.
When I say "weird", I mean that behavior of the program does not correspond to your intuitive expectation.
From here, one can argue that "safe" means "corresponding to my intuition", and therefore the way to determine when I'm approaching the edges is when the behavior starts to diverge from my intuition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. What I'm asking is: do you have any heuristics for knowing when using list monads/applicatives might behave in unintuitive ways?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I wouldn't do anything more complex than several <- lines in a row, like:
foo xs = do
x <- xs
y <- getYs x
z <- getZs x y
pure $ bar x y z
| -- (https://github.com/maxrothman/fp-course/blob/e20fcd61979b76778836dd5277181937ad6facc3/test/Course/StateTTest.hs#L133) | ||
| -- Mine did too when I had `a <- fa` in the top do. Moving it to the bottom do fixed the issue. | ||
| -- | ||
| -- Also not really sure what the suggestion to use onFull was about, they don't use it either. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know what onFull is. I can guess that it is probably a catamorphism for Optional, i.e. something like onFull :: b -> (a -> b) -> Optional a -> b. If so, you can replace the case expression with an onFull call, but I'm not sure it would look any better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's defined below: onFull :: Applicative f => (t -> f (Optional a)) -> Optional t -> f (Optional a)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see how this is different from map
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It basically just puts a case statement around the second argument
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks exactly like fmap, only with the second type parameter specialized to Optional a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
¯\_(ツ)_/¯
| case maybeA of | ||
| Empty -> pure Empty | ||
| Full a -> runOptionalT . func $ a | ||
| -- REVIEW: un-OptionalT'ing this to just re-OptionalT it again seems dumb. There's no |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, that's the essence of what =<< does: it unwraps, applies the function, then rewraps. If you can do it in a clever way that doesn't actually involved unwrapping/rewrapping - more power to you, but in general this is indeed impossible. Look at MaybeT for example.
| Empty -> pure Empty | ||
| Full a -> runOptionalT . func $ a | ||
| -- REVIEW: un-OptionalT'ing this to just re-OptionalT it again seems dumb. There's no | ||
| -- straightforward way around it, right? And I can assume the extra work will be optimized away? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The extra work is indeed guaranteed to be optimized away, because OptionalT is a newtype.
No description provided.