Skip to content

Conversation

@maxrothman
Copy link
Owner

No description provided.

mstruebing and others added 30 commits February 10, 2018 18:11
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.
by completing the custom setup stanza
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
Make gitignore ignore cabal artifacts in any subdirectory
Remove extra argument of property for ListZipper.index
Fix typo in Course.Optional#optional
Fix error typo in Course.MoreParser#string
dalaing and others added 24 commits November 23, 2018 10:12
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.
(<$$>) =
error "todo: Course.Comonad#(<$>)"
func <$$> fa = func . copure <<= fa
-- REVIEW: would it be desireable to eta-reduce this to `(<$$>) f = (f . copure <<=)` ?
Copy link

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
Copy link

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?
Copy link

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.

Copy link
Owner Author

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?

Copy link

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?
Copy link

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.

Copy link
Owner Author

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.

Copy link

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
Copy link

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.

Copy link
Owner Author

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?

Copy link

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
Copy link

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.

Copy link

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.

Copy link
Owner Author

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?

Copy link

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.

Copy link
Owner Author

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 lift2 to solve this problem
  • Something else?

Copy link

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.

Copy link
Owner Author

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?

Copy link

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.

Copy link
Owner Author

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?

Copy link

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

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.

Copy link
Owner Author

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)

Copy link

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

Copy link
Owner Author

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

Copy link

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

Copy link
Owner Author

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
Copy link

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?
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.