Это может быть сложным делом, но в некоторых контекстах это выполнимо. По сути, если это монады, которые вы видите внутри (например, Maybe
или написанная вами монада), то вы можете определить такую операцию.
Одна вещь, которая иногда весьма удобна (в GHC) - это заменить класс Monad
на свой собственный. Если вы определите return, >>=, fail
, вы все равно сможете использовать запись do
. Вот пример, который может быть таким, как вы хотите:
class Compose s t where
type Comp s t
class Monad m where
return :: a -> m s a
fail :: String -> m a
(>>=) :: (Compose s t) => m s a -> (a -> m t b) -> m (Comp s t) b
(>>) :: (Compose s t) => m s a -> m t b -> m (Comp s t) b
m >> m' = m >>= \_ -> m'
Затем вы можете контролировать, какие типы могут быть упорядочены, используя оператор связывания, основываясь на том, какие экземпляры Compose
вы определяете. Естественно, вы часто будете хотеть Comp s s = s
, но вы также можете использовать это, чтобы определить все виды сумасшедших вещей.
Например, возможно, у вас есть какие-то операции в вашей монаде, за которыми совершенно не могут следовать никакие другие операции. Хотите применить это статически? Определите пустой тип данных data Terminal
и не предоставляйте экземпляры Compose Terminal t
.
Этот подход не подходит для переноса (скажем) Maybe
в IO
, но его можно использовать для переноса некоторых данных на уровне типов о том, что вы делаете.
Если вы действительно хотите изменить монады, вы можете изменить приведенные выше определения классов примерно на
class Compose m n where
type Comp m n
(>>=*) :: m a -> (a -> n b) -> (Compose m n) b
class Monad m where
return :: a -> m a
fail :: String -> m a
(>>=) :: Compose m n => m a -> (a -> n b) -> (Compose m n) b
m >>= f = m >>=* f
(>>) :: Compose m n => m a -> (n b) -> (Compose m n) b
m >> n = m >>=* \_ -> n
Я использовал предыдущий стиль для полезных целей, хотя я полагаю, что эта последняя идея также может быть полезна в определенных контекстах.