2

I have a function that pattern matches on its arguments to produce a computation in StateT () Maybe (). This computation can fail when run, in which case I want the current pattern match branch to fail, so to speak.

I highly doubt it's possible to have something like

compute :: Int -> StateT () Maybe Int
compute = return

f :: Maybe Int -> Maybe Int -> StateT () Maybe ()
f (Just n1) (Just n2) = do
  m <- compute (n1 + n2) 
  guard (m == 42)
f (Just n) _ = do
  m <- compute n
  guard (m == 42)
f _ (Just n) = do
  m <- compute n
  guard (m == 42)

behave in the way I want it to: When the first computation fails due to the guard or somewhere in compute, I want f to try the next pattern.

Obviously the above can't work, because StateT (as any other monad might) involves an additional parameter when expanded, so I probably can't formulate this as simple pattern guards.

The following does what I want, but it's ugly:

f' :: Maybe Int -> Maybe Int -> StateT () Maybe ()
f' a b = asum (map (\f -> f a b) [f1, f2, f3])
  where
    f1 a b = do
      Just n1 <- pure a
      Just n2 <- pure b
      m <- compute (n1 + n2) 
      guard (m == 42)
    f2 a _ = do
      Just n <- pure a
      m <- compute n
      guard (m == 42)
    f3 _ b = do
      Just n <- pure b
      m <- compute n
      guard (m == 42)

A call like execStateT (f (Just 42) (Just 1)) () would fail for f but return Just () for f', because it matches f2.

How do I get the behavior of f' while having elegant pattern matching with as little auxiliary definitions as possible like in f? Are there other, more elegant ways to formulate this?


Complete runnable example:

#! /usr/bin/env stack
-- stack --resolver=lts-11.1 script

import Control.Monad.Trans.State
import Control.Applicative
import Control.Monad
import Data.Foldable

compute :: Int -> StateT () Maybe Int
compute = return

f :: Maybe Int -> Maybe Int -> StateT () Maybe ()
f (Just n1) (Just n2) = do
  m <- compute (n1 + n2) 
  guard (m == 42)
f (Just n) _ = do
  m <- compute n
  guard (m == 42)
f _ (Just n) = do
  m <- compute n
  guard (m == 42)

f' :: Maybe Int -> Maybe Int -> StateT () Maybe ()
f' a b = asum (map (\f -> f a b) [f1, f2, f3])
  where
    f1 a b = do
      Just n1 <- pure a
      Just n2 <- pure b
      m <- compute (n1 + n2) 
      guard (m == 42)
    f2 a _ = do
      Just n <- pure a
      m <- compute n
      guard (m == 42)
    f3 _ b = do
      Just n <- pure b
      m <- compute n
      guard (m == 42)

main = do
  print $ execStateT (f (Just 42) (Just 1)) ()  -- Nothing
  print $ execStateT (f' (Just 42) (Just 1)) () -- Just (), because `f2` succeeded

Edit: I elicited quite some clever answers with this question so far, thanks! Unfortunately, they mostly suffer from overfitting to the particular code example I've given. In reality, I need something like this for unifying two expressions (let-bindings, to be precise), where I want to try unifying the RHS of two simultaneous lets if possible and fall through to the cases where I handle let bindings one side at a time by floating them. So, actually there's no clever structure on Maybe arguments to exploit and I'm not computeing on Int actually.

The answers so far might benefit others beyond the enlightenment they brought me though, so thanks!


Edit 2: Here's some compiling example code with probably bogus semantics:

module Unify (unify) where

import Control.Applicative
import Control.Monad.Trans.State.Strict

data Expr
  = Var String -- meta, free an bound vars
  | Let String Expr Expr
  -- ... more cases
  -- no Eq instance, fwiw

-- | If the two terms unify, return the most general unifier, e.g.
-- a substitution (`Map`) of meta variables for terms as association
-- list.
unify :: [String] -> Expr -> Expr -> Maybe [(String, Expr)]
unify metaVars l r = execStateT (go [] [] l r) [] -- threads the current substitution as state
  where
    go locals floats (Var x) (Var y)
      | x == y = return ()
    go locals floats (Var x) (Var y)
      | lookup x locals == Just y = return ()
    go locals floats (Var x) e
      | x `elem` metaVars = tryAddSubstitution locals floats x e
    go locals floats e (Var y)
      | y `elem` metaVars = tryAddSubstitution locals floats y e
    -- case in point:
    go locals floats (Let x lrhs lbody) (Let y rrhs rbody) = do
      go locals floats lrhs rrhs -- try this one, fail current pattern branch if rhss don't unify
      -- if we get past the last statement, commit to this branch, no matter
      -- the next statement fails or not
      go ((x,y):locals) floats lbody rbody
    -- try to float the let binding. terms mentioning a floated var might still
    -- unify with a meta var
    go locals floats (Let x rhs body) e = do
      go locals (Left (x,rhs):floats) body e
    go locals floats e (Let y rhs body) = do
      go locals (Right (y,rhs):floats) body e

    go _ _ _ _ = empty

    tryAddSubstitution = undefined -- magic
Sebastian Graf
  • 3,602
  • 3
  • 27
  • 38
  • Perhaps this is related: https://stackoverflow.com/questions/28526768/is-there-in-haskell-something-similar-to-sub-guards – chi Mar 27 '18 at 09:38
  • "computation can fail when run, in which case I want the whole pattern match to fail, so to speak" seem to contradict with "When the first computation fails due to the guard or somewhere in compute, I want f to try the next pattern". If you want to have 2 types of failures - permanent and try next pattern, then you should have 2 layers of Maybe – max630 Mar 27 '18 at 12:03
  • 1
    I think it's pretty clear from the code he means that the computation under the current branch should fail. – Li-yao Xia Mar 27 '18 at 12:19
  • Exactly. I edited that part. – Sebastian Graf Mar 27 '18 at 12:26
  • What is your case spliting pattern for a b? Maybe it can be done another way. Something like generateCases a b >>= asum commute – Johnny Liao Mar 27 '18 at 13:52
  • Edit: generateCases a b >>= asum (commute >>= guard (==42)) – Johnny Liao Mar 27 '18 at 14:07
  • Regarding your edit, could you perhaps give such a more complex example? – Petr Mar 31 '18 at 01:39
  • I added a more concrete unification example. – Sebastian Graf Mar 31 '18 at 10:47

3 Answers3

3

If you were using Maybe alone, you would be able to do this with pattern guards:

import Control.Monad
import Control.Applicative

ensure :: Alternative f => (a -> Bool) -> a -> f a
ensure p a = a <$ guard (p a)

compute :: Int -> Maybe Int
compute = return

f :: Maybe Int -> Maybe Int -> Maybe Int
f (Just m) (Just n)
    | Just x <- ensure (== 42) =<< compute (m + n)
    = return x
f (Just m) _
    | Just x <- ensure (== 42) =<< compute m
    = return x
f _ (Just n)
    | Just x <- ensure (== 42) =<< compute n
    = return x
f _ _ = empty

(ensure is a general purpose combinator. Cf. Lift to Maybe using a predicate)

As you have StateT on the top, though, you would have to supply a state in order to pattern match on Maybe, which would foul up everything. That being so, you are probably better off with something in the vein of your "ugly" solution. Here is a whimsical attempt at improving its looks:

import Control.Monad
import Control.Applicative
import Control.Monad.State
import Control.Monad.Trans
import Data.Foldable

ensure :: Alternative f => (a -> Bool) -> a -> f a
ensure p a = a <$ guard (p a)

compute :: Int -> StateT () Maybe Int
compute = return

f :: Maybe Int -> Maybe Int -> StateT () Maybe Int
f a b = asum (map (\c -> f' (c a b)) [liftA2 (+), const, flip const])
    where
    f' = ensure (== 42) <=< compute <=< lift

While this is an answer specific to the snippet I've given, the refactorings only apply limited to the code I was facing.

Perhaps it's not that far-fetched of an idea to extract the skeleton of the asum expression above to a more general combinator:

-- A better name would be welcome.
selector :: Alternative f => (a -> a -> a) -> (a -> f b) -> a -> a -> f b
selector g k x y = asum (fmap (\sel -> k (sel x y)) [g, const, flip const])

f :: Maybe Int -> Maybe Int -> StateT () Maybe Int
f = selector (liftA2 (+)) (ensure (== 42) <=< compute <=< lift)

Though it is perhaps a bit awkward of a combinator, selector does show the approach is more general than it might appear at first: the only significant restriction is that k has to produce results in some Alternative context.

P.S.: While writing selector with (<|>) instead of asum is arguably more tasteful...

selector g k x y = k (g x y) <|> k x <|> k y

... the asum version straightforwardly generalises to an arbitrary number of pseudo-patterns:

selector :: Alternative f => [a -> a -> a] -> (a -> f b) -> a -> a -> f b
selector gs k x y = asum (fmap (\g -> k (g x y)) gs)
duplode
  • 33,731
  • 7
  • 79
  • 150
  • See my last edit. While this is an answer specific to the snippet I've given, the refactorings only apply limited to the code I was facing. This was helpful nonetheless! – Sebastian Graf Mar 28 '18 at 15:07
  • @SebastianGraf Though I might be missing something, I think this strategy is more general than it might appear at first sight. See my latest edit. – duplode Mar 29 '18 at 12:39
  • I don't think I can factor my `compute`ation into an `a -> a -> a` and a `a -> f b`, because the branches have quite different logic actually, which probably leaves me with something like the other answer. – Sebastian Graf Mar 29 '18 at 14:49
3

When I need something like this, I just use asum with the blocks inlined. Here I also condensed the multiple patterns Just n1 <- pure a; Just n2 <- pure b into one, (Just n1, Just n2) <- pure (a, b).

f :: Maybe Int -> Maybe Int -> StateT () Maybe ()
f a b = asum

  [ do
    (Just n1, Just n2) <- pure (a, b)
    m <- compute (n1 + n2) 
    guard (m == 42)

  , do
    Just n <- pure a
    m <- compute n
    guard (m == 42)

  , do
    Just n <- pure b
    m <- compute n
    guard (m == 42)

  ]

You can also use chains of <|>, if you prefer:

f :: Maybe Int -> Maybe Int -> StateT () Maybe ()
f a b

  = do
    (Just n1, Just n2) <- pure (a, b)
    m <- compute (n1 + n2) 
    guard (m == 42)

  <|> do
    Just n <- pure a
    m <- compute n
    guard (m == 42)

  <|> do
    Just n <- pure b
    m <- compute n
    guard (m == 42)

This is about as minimal as you can get for this kind of “fallthrough”.

Jon Purdy
  • 53,300
  • 8
  • 96
  • 166
  • I think I'll go with something like this in the end. But I'll wait for some more input before I decide which answer to accept. – Sebastian Graf Mar 28 '18 at 15:07
1

It looks like you could get rid of the whole pattern match by relying on the fact that Int forms a Monoid with addition and 0 as the identity element, and that Maybe a forms a Monoid if a does. Then your function becomes:

f :: Maybe Int -> Maybe Int -> StateT () Maybe Int
f a b = pure $ a <> b >>= compute >>= pure . mfilter (== 42)

You could generalise by passing the predicate as an argument:

f :: Monoid a => (a -> Bool) -> Maybe a -> Maybe a -> StateT () Maybe a
f p a b = pure $ a <> b >>= compute >>= pure . mfilter p

The only thing is that compute is now taking a Maybe Int as input, but that is just a matter of calling traverse inside that function with whatever computation you need to do.


Edit: Taking into account your last edit, I find that if you spread your pattern matches into separate computations that may fail, then you can just write

f a b = f1 a b <|> f2 a b <|> f3 a b
  where f1 (Just a) (Just b) = compute (a + b) >>= check
        f1 _        _        = empty
        f2 (Just a) _        = compute a >>= check
        f2 _        _        = empty
        f3 _        (Just b) = compute b >>= check
        f3 _        _        = empty
        check x              = guard (x == 42)
Regis Kuckaertz
  • 991
  • 5
  • 14
  • 1
    See my latest edit at the bottom of the question. I'm afraid this solution is too overfitted to the code snippet I've given. – Sebastian Graf Mar 28 '18 at 15:09
  • No worries, thanks for the update. In effect, you're saying there is no `Maybe Int` but instead some `f a` representing RHS expressions, am I right? Another observation is that your computation does not care which branch of the pattern match is taken: does that still hold? – Regis Kuckaertz Mar 29 '18 at 06:29
  • The computation is actually different for each branch. But something like what you suggested is what I would end up writing in absence of a better idea. – Sebastian Graf Mar 29 '18 at 14:44