5.5 KiB
Phillip
Hey man, long time no... read? Having seen your comment, I don't have anything exceptionally eye-opening to contribute, but here goes:
-
At first glance, it seemed like it should be easy to simplify all those 4-deep case statements into a single line, but I don't think that's quite the case. I think that if you were to just rewrite the
Maybecode using Haskell's standard functions, you would need to useMaybe'sMonadinstance, even though I didn't need it for myApplicativeparser data type. The difference is in the types. The result of a parser application isMaybe (a, String), and thatStringargument is used by the next parser.Applicative, on the other hand, does not support making decisions based on the data inside the functor. The signatures forfmapand<*>are(a -> b) -> f a -> f bandm (a -> b) -> m a -> mb: you have to have both the function and its arguments before you combine them.On the other hand, when turning Parser into its own data type, the
Stringstate-passing can be hidden away, so instead ofMaybe (a, String)you'll just haveParser a. At the type signature level, you no longer rely on the "state" (leftover string) in your combinators, so you only needApplicative.In short, with
Maybe, I think the best you can do is something like the following:do (_, s1) <- char '(' s (i, s2) <- nat s1 ...Perhaps this reminds you of the implementation of the State monad? My intuition is that a Parser is just a combination of the
StateandErrormonads. -
I think that your implementation of
natPairandnatTreecould be refactored a little bit. In particular, you can abstract the code for parsing "two things in parentheses separated by a comma", perhaps into a function likepair :: Parser a -> Parser b -> Parser (a, b). If you did that, your 4-deep chain of case analysis would only occur in one place (inpair), and your other two functions would just call out to it. Applying just this refactoring step, you'd get:natPair = pair nat nat natTree s = case pair natTree natTree s of Just ((t1, t2), s') -> Just $ (Node t1 t2, s') Nothing -> case nat s of Just (n, s') -> Just $ (Leaf n, s') Nothing -> NothingThis has all the usual benefits of abstraction which I won't bore you with :-)
Jack
Hey, sorry to see you didn't have time to finish up natTree. I've got a few comments:
-
Your
(<|>)implementation is actually nearly identical toMaybe's implementation ofAlternative's(<|>). In particular, you're effectively (lazily) combining twoMaybevalues, one fromp1and one fromp2. Thus, you can actually write that whole function asp1 (<|>) p2 = \s -> p1 s (<|>) p2 s. Well, except that then you have an ambiguous reference to(<|>), so you have to qualify it, likeControl.Applicative.(<|>). -
You probably know this, but your helper functions
parseMap,ifTranP, andaddPare specializations of the standard functionsfmap,(>>=), andliftA2. In particular,addPis pretty muchliftA2 (:). This does, of course, rely on theFunctor,Monad, andApplicativeinstances being defined for theParserdata type, which requires a bit of handywork given the starter code. The advantage, though, is getting access to all these fancy combinators from the standard library (like*>and<*). Similarly, your\s -> Just ([], s)could be written asreturn []. -
Our
natfunctions are practically identical! I went with pointfree style again (I have a bit of a problem, pointfree is not very readable at all), but other than that, it's scary how close our answers are! -
The whole "early return" pattern (check for
Just, compute nextMaybe, check forJustagain) can at the very least be simplified as:natPair s1 = do (_, s2) <- char '(' s1 (first, s3) <- nat s2 (_, s4) <- char ',' s3 (second, s5) -> case nat s4 (_, s6) <- char ')' s5 return $ ((first, second), s6)But wait a moment... we didn't actually do anything with the values of
firstandsecond! This means that we can generalize this function just a little bit (replacenatbut an arbitrary input parser):pair p1 p2 s1 = do (_, s2) <- char '(' s1 (first, s3) <- p1 s2 (_, s4) <- char ',' s3 (second, s5) -> case p2 s4 (_, s6) <- char ')' s5 return $ ((first, second), s6)Now,
natPaircan be written aspair nat nat(you can even verify this by some straightforward equational reasoning). And now that you have that, you can also definenatTree. The first version:natTree = pair natTree natTreeAlas, this is of type
Parser (Tree, Tree), notParser Tree. To combine the two trees into one, we can use yourparseMap:natTree = parseMap (uncurry Node) (pair natTree natTree)Oh, but we're missing a base case! We can use the
(<|>)operator we defined earlier to define a "fallback" if we can't parse another level of the tree.natTree = parseMap (uncurry Node) (pair natTree natTree) <|> parseMap Leaf natTwo birds with one stone, right? Both
natPairandnatTreeknocked out by a singlepairfunction. It's true that definingnatPairis quite messy, and hard to expand intonatTree, but stuffing all that complexity into a helper function helps keep that messiness at bay :-)