Передайте линзу в функцию

Как правильно передать линзу в функцию с состоянием? Рассмотрим следующий код:

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}

import Control.Lens
import Control.Monad.State

data Game = Game { _armies :: [Army]
                 } deriving (Show)

data Army = Army { _troops :: Int
                 } deriving (Show)

makeLenses ''Game
makeLenses ''Army

data BattleResult = Win | Defeat deriving (Show)

offend offender defender = do
  Just ot <- preuse $ offender.troops
  Just dt <- preuse $ defender.troops
  defender.troops.=0 -- doesn't work
  let eval a b
        | a >= b    = return Win
        | otherwise = return Defeat
  eval ot dt

game :: State Game ()
game = do
    armies %= (:) (Army 100)
    armies %= (:) (Army 200)
    q <- offend (armies.ix 0) (armies.ix 1)
    return ()

Отмеченная строка ведет к следующей ошибке:

Lens.hs:21:3:
    Couldn't match type ‘Const (Data.Monoid.First Int) s’
                   with ‘Identity s’
    Expected type: (Army -> Const (Data.Monoid.First Int) Army)
                   -> s -> Identity s
      Actual type: (Army -> Const (Data.Monoid.First Int) Army)
                   -> s -> Const (Data.Monoid.First Int) s
    Relevant bindings include
      defender :: (Army -> Const (Data.Monoid.First Int) Army)
                  -> s -> Const (Data.Monoid.First Int) s
        (bound at Lens.hs:18:17)
      offender :: (Army -> Const (Data.Monoid.First Int) Army)
                  -> s -> Const (Data.Monoid.First Int) s
        (bound at Lens.hs:18:8)
      offend :: ((Army -> Const (Data.Monoid.First Int) Army)
                 -> s -> Const (Data.Monoid.First Int) s)
                -> ((Army -> Const (Data.Monoid.First Int) Army)
                    -> s -> Const (Data.Monoid.First Int) s)
                -> m BattleResult
        (bound at Lens.hs:18:1)
    In the first argument of ‘(.)’, namely ‘defender’
    In the first argument of ‘(.=)’, namely ‘defender . troops’

Lens.hs:21:12:
    Couldn't match type ‘Identity Integer’
                   with ‘Const (Data.Monoid.First Int) Int’
    Expected type: (Int -> Identity Integer)
                   -> Army -> Const (Data.Monoid.First Int) Army
      Actual type: (Int -> Const (Data.Monoid.First Int) Int)
                   -> Army -> Const (Data.Monoid.First Int) Army
    In the second argument of ‘(.)’, namely ‘troops’
    In the first argument of ‘(.=)’, namely ‘defender . troops’

Если заменить строку на что-то вроде armies.ix 0.troops.=0, код нормально компилируется. Есть ли какие-то стандартные инструменты для обхода проблемы? И можно ли реализовать тот же алгоритм без использования FlexibleContexts?


person Leonid    schedule 05.03.2016    source источник
comment
Этот let eval ... немного странный. Почему бы не пропустить это и просто написать if a >= b then return Win else return Defeat?   -  person dfeuer    schedule 06.03.2016
comment
@dfeuer, если мне нужно больше случаев (например, ›, ‹ и ==), я должен использовать это выражение let. Знаете ли вы какие-либо другие способы реализации защиты внутри блока do?   -  person Leonid    schedule 06.03.2016
comment
case () of _ | ... это классика. GHC MultiWayIf предлагает для этого лучший синтаксис.   -  person dfeuer    schedule 06.03.2016


Ответы (1)


Просто используйте подписи типов!

Что здесь происходит: если вы не предоставите подпись, GHC сможет сделать вывод только о типе Rank-1. В этом примере вы используете defender.troops в качестве геттера; поэтому компилятор выводит тип получателя для defender. Это уродливое сообщение об ошибке с Const в нем.

Однако вы также хотите использовать его в качестве сеттера. Это возможно только в том случае, если defender является полиморфным (так что вы можете использовать функтор Identity вместо Const), а для того, чтобы аргумент был полиморфным, вам нужен полиморфизм ранга-2.

Вам действительно не нужно беспокоиться об этом виде магии теории категорий, поскольку библиотека объективов предоставляет простые в использовании синонимы. Просто напишите подпись как следует всегда делать в любом случае,

offend :: Traversal' Game Army -> Traversal' Game Army -> State Game BattleResult

и вы получите правильные полиморфные аргументы. Ах, и, конечно же, вам нужно расширение -XRankNTypes. -XFlexibleContexts на самом деле не требуется (хотя он совершенно безвреден, нет причин его не использовать).


Хиндли-Милнер в любом случае является чудом, если вы спросите меня, но он работает только потому, что для любого выражения существует четко определенная наиболее общая возможная подпись. Однако это относится только к коду ранга-1: с рангом-N вы всегда можете добавить еще один уровень универсальной количественной оценки. Компилятор не знает, когда это закончить!

На самом деле это Getting, который является обходным геттером. Разница между Getter и Getting заключается в том, что последний может быть частичным (что необходимо, потому что вы используете ix; компилятор не может доказать, что на самом деле есть элемент с индексом 1 в списке армий).

person leftaroundabout    schedule 05.03.2016
comment
Еще один вопрос. Когда я пытаюсь поместить строку defender.troops.=0 во вложенный блок do перед return Win, компилятор выдает ошибку и предлагает снова использовать FlexibleContexts. Как это сделать без этого расширения? - person Leonid; 05.03.2016
comment
А есть что-то лучше preuse? Я использую его в надлежащем месте? - person Leonid; 05.03.2016
comment
Действительно? Вложенные блоки do не должны иметь большого значения здесь... но если это делает компилятор счастливым, черт возьми, просто включите -XFlexibleContexts, как я уже сказал, я не вижу веских причин не делать этого. — preuse здесь нормально, я бы тоже его использовал. - person leftaroundabout; 05.03.2016