对于可能作为 setter 失败的镜头,适当的抽象是什么?

What is the appropriate abstraction for a lens which can fail as a setter?

我想定义一个镜头之类的东西,但在尝试设置时可能会失败。请参阅以下示例中的 fooLens

{-# LANGUAGE RankNTypes #-}

import Data.Char (toUpper)
import Data.Functor.Const (Const(..))
import Data.Functor.Identity (Identity(..))

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
type Getting r s t a = (a -> Const r a) -> s -> Const r t

view :: Getting a s t a -> s -> a
view l = getConst . l Const

over :: Lens s t a b -> (a -> b) -> s -> t
over l f = runIdentity . l (Identity . f)

data Foo a = Foo a deriving (Show)

fooLens :: Lens (Foo a) (Either String (Foo a)) a a
fooLens f (Foo a) = Right . Foo <$> f a

main = do
    let foo = Foo "test"
    print foo
    print $ view fooLens foo
    print $ over fooLens (map toUpper) foo

这就是您所期望的结果

Foo "test"
"test"
Right (Foo "TEST")

我在这里概括了 Getting 的定义以使其有效。首先要明确的是 fooLens 不是透镜:它不满足透镜定律。相反,它是由透镜和类似棱镜的东西组成的。

这似乎可行,但事实上我检查过的任何镜头库都不支持它,这表明可能有更好的方法来解决这个问题。有没有办法重构 fooLens 以便它:

  1. 充当 getter,即它始终可以检索值。
  2. 可以充当 setter,但有可能会失败,例如 returns 一个 Either。

我认为这是因为有一个未说明的 profunctor 光学类型级定律。 s t a b 光学类型需要满足类型级别法则:a ~ b 意味着 s ~ t.

因此,Getting 没有被泛化,因为它的类型有 a ~ b,这意味着 s ~ t。同样,fooLens 不是已知的光学器件,因为它违反了这条定律,所以它有点不可能。

正如我所说,我从未见过这种类型级别的法律是明确的,但我认为它是隐含的。

您的特定配方在镜片生态系统中效果不佳。镜头所做的最重要的事情是提供不同类型的光学组合。为了演示,让我们从您的代码的一个稍微修饰的版本开始:

{-# LANGUAGE RankNTypes #-}

import Data.Char (toUpper)
import Data.Functor.Const (Const(..))
import Data.Functor.Identity (Identity(..))

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
type Getting r s t a = (a -> Const r a) -> s -> Const r t

view :: Getting a s t a -> s -> a
view l = getConst . l Const

over :: Lens s t a b -> (a -> b) -> s -> t
over l f = runIdentity . l (Identity . f)

data Foo a = Foo a
    deriving (Show, Eq, Ord)

fooLens :: Lens (Foo [a]) (Either String (Foo [a])) [a] [a]
fooLens f (Foo a) = update <$> f a
  where
    update x | null x = Left "Cannot be empty"
             | otherwise = Right (Foo x)

main = do
    let foo = Foo "test"
    print foo
    print $ view fooLens foo
    print $ over fooLens (map toUpper) foo
    print $ over fooLens (const "") foo

输出为:

Foo "test"
"test"
Right (Foo "TEST")
Left "Cannot be empty"

我修改了 fooLens 一点以充分利用其类型,在更新时验证数据。这有助于说明此公式的目标。

然后我决定测试它的合成效果,并添加了以下内容:

data Bar = Bar (Foo String)
    deriving (Show, Eq, Ord)

barLens :: Lens Bar Bar (Foo String) (Foo String)
barLens f (Bar x) = Bar <$> f x

然后将以下内容添加到 main

    print $ view (barLens . fooLens) (Bar foo)

它只是不组成:

error:
    • Couldn't match type ‘Either String (Foo [Char])’
                     with ‘Foo String’
      Expected type: ([Char] -> Const [Char] [Char])
                     -> Foo String -> Const [Char] (Foo String)
        Actual type: ([Char] -> Const [Char] [Char])
                     -> Foo [Char] -> Const [Char] (Either String (Foo [Char]))
    • In the second argument of ‘(.)’, namely ‘fooLens’
      In the first argument of ‘view’, namely ‘(barLens . fooLens)’
      In the second argument of ‘($)’, namely
        ‘view (barLens . fooLens) (Bar foo)’
   |
37 |     print $ view (barLens . fooLens) (Bar foo)
   |                             ^^^^^^^

仅此一项就足以防止在镜头中使用该配方。它不符合图书馆的目标。

让我们尝试一些不同的东西。这不是您要查找的内容,但它是一个观察结果。

import Control.Lens

data Foo a = Foo a
    deriving (Show, Eq, Ord)

fooLens :: Lens (Foo [a]) (Foo [a]) [a] [a]
fooLens f (Foo a) = update <$> f a
  where
    update x | null x = Foo a
             | otherwise = Foo x

main :: IO ()
main = do
    let foos = map Foo $ words "go fly a kite"
    print foos
    print $ toListOf (traverse . fooLens) foos
    print $ over (traverse . fooLens) tail foos
    print =<< (traverse . fooLens) (\x -> tail x <$ print x) foos

输出:

[Foo "go",Foo "fly",Foo "a",Foo "kite"]
["go","fly","a","kite"]
[Foo "o",Foo "ly",Foo "a",Foo "ite"]
"go"
"fly"
"a"
"kite"
[Foo "o",Foo "ly",Foo "a",Foo "ite"]

显然这不是真正的镜头,可能应该有一个不同的名字,因为它不遵守 set-view 法则。可以用相同的类型来写有点尴尬,但是像 filtered.

这样的东西是有先例的

但是还有一个更复杂的问题,正如上次测试所证明的那样 - 即使更新被拒绝,过滤更新结果仍然需要 运行 更新的效果。这不是跳过元素的方式,例如 filteredTraversal 中的工作原理。这似乎无法通过 van Laarhoven 代表来避免。但也许这还不错。这在设置或查看时不是问题 - 只有在执行不太常见的操作时才存在。

无论如何,它不会报告设置失败,所以这不是您要查找的内容。但经过足够的重新调整,它可以成为一个起点。

{-# LANGUAGE
        MultiParamTypeClasses,
        FlexibleInstances,
        TypeFamilies,
        UndecidableInstances,
        FlexibleContexts #-}

import Data.Functor.Identity
import Control.Applicative
import Control.Monad

import Control.Lens



class Functor f => Reportable f e where
    report :: a -> f (Either e a) -> f a

instance Reportable (Const r) e where
    report _ (Const x) = Const x

instance Reportable Identity e where
    report a (Identity i) = Identity $ either (const a) id i

instance (e ~ a) => Reportable (Either a) e where
    report _ = join

overWithReport
    :: ((a -> Either e b) -> s -> Either e t)
    -> (a -> b)
    -> s
    -> Either e t
overWithReport l f s = l (pure . f) s



data Foo a = Foo a
    deriving (Show, Eq, Ord)

fooLens
    :: (Reportable f String)
    => ([a] -> f [a])
    -> Foo [a]
    -> f (Foo [a])
fooLens f (Foo a) = report (Foo a) $ update <$> f a
  where
    update x | null x = Left "Cannot be empty"
             | otherwise = Right $ Foo x



main :: IO ()
main = do
    let foos = [Foo [1], Foo [2, 3]]
    print foos

    putStrLn "\n  Use as a normal lens:"
    print $ toListOf (traverse . fooLens . traverse) foos
    print $ over (traverse . fooLens . traverse) (+ 10) foos
    print $ over (traverse . fooLens) tail foos

    putStrLn "\n  Special use:"
    print $ overWithReport (traverse . fooLens . traverse) (+ 10) foos
    print $ overWithReport (traverse . fooLens) (0 :) foos
    print $ overWithReport (traverse . fooLens) tail foos

这是 运行 它的输出:

[Foo [1],Foo [2,3]]

  Use as a normal lens:
[1,2,3]
[Foo [11],Foo [12,13]]
[Foo [1],Foo [3]]

  Special use:
Right [Foo [11],Foo [12,13]]
Right [Foo [0,1],Foo [0,2,3]]
Left "Cannot be empty"

此配方与普通镜片材料相结合。它可以工作,但需要对 over 进行修改才能获得错误报告。它保持与许多镜头功能的兼容性,在一种情况下以一些非法行为为代价。它并不完美,但在保持与镜头库其余部分的兼容性的限制下,它可能尽可能接近。

至于为什么这些内容不在库中,可能是因为它需要对 f 类型别名进行自定义约束,这对于使用像 [=26 这样的组合器来说是一个真正的麻烦=].我为 IdentityConst 提供的实例处理了镜头本身的大部分用途,但还有更多人可能会选择使用它。

镜头库的开放式设计允许进行大量的外部定制。这是一种可能适用于很多情况的可能方法。但它的工作范围比镜头允许的范围要小得多,我认为这就是为什么目前没有这样的东西。