Как сделать этот алгоритм более ленивым, не повторяя себя?


9

(Вдохновлен моим ответом на этот вопрос .)

Рассмотрим этот код (он должен найти самый большой элемент, который меньше или равен заданному входу):

data TreeMap v = Leaf | Node Integer v (TreeMap v) (TreeMap v) deriving (Show, Read, Eq, Ord)

closestLess :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess i = precise Nothing where
  precise :: Maybe (Integer, v) -> TreeMap v -> Maybe (Integer, v)
  precise closestSoFar Leaf = closestSoFar
  precise closestSoFar (Node k v l r) = case i `compare` k of
    LT -> precise closestSoFar l
    EQ -> Just (k, v)
    GT -> precise (Just (k, v)) r

Это не очень лениво. После того, как GTдело введено, мы точно знаем, что окончательное возвращаемое значение будет Justчем-то Nothing, а Justне до конца. Я хотел бы сделать это более ленивым, чтобы Justон был доступен, как только GTдело введено. Мой тестовый пример для этого, что я хочу Data.Maybe.isJust $ closestLess 5 (Node 3 () Leaf undefined)оценить, Trueа не дно. Вот один из способов сделать это:

data TreeMap v = Leaf | Node Integer v (TreeMap v) (TreeMap v) deriving (Show, Read, Eq, Ord)

closestLess :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess _ Leaf = Nothing
closestLess i (Node k v l r) = case i `compare` k of
  LT -> closestLess i l
  EQ -> Just (k, v)
  GT -> Just (precise (k, v) r)
  where
    precise :: (Integer, v) -> TreeMap v -> (Integer, v)
    precise closestSoFar Leaf = closestSoFar
    precise closestSoFar (Node k v l r) = case i `compare` k of
      LT -> precise closestSoFar l
      EQ -> (k, v)
      GT -> precise (k, v) r

Однако сейчас я повторяюсь: основная логика теперь closestLessи в, и в precise. Как я могу написать это так, чтобы это было лениво, но не повторялось?

Ответы:


4

Вместо того, чтобы использовать явные оболочки, вы можете использовать систему типов. Обратите внимание, что версия, preciseкоторая используется Maybeдля вашего первого фрагмента кода:

precise :: Maybe (Integer, v) -> TreeMap v -> Maybe (Integer, v)
precise closestSoFar Leaf = closestSoFar
precise closestSoFar (Node k v l r) = case i `compare` k of
  LT -> precise closestSoFar l
  EQ -> Just (k, v)
  GT -> precise (Just (k, v)) r

это почти тот же алгоритм, что и версия preciseбез Maybeвашего второго фрагмента кода, которая может быть записана в Identityфункторе как:

precise :: Identity (Integer, v) -> TreeMap v -> Identity (Integer, v)
precise closestSoFar Leaf = closestSoFar
precise closestSoFar (Node k v l r) = case i `compare` k of
  LT -> precise closestSoFar l
  EQ -> Identity (k, v)
  GT -> precise (Identity (k, v)) r

Они могут быть объединены в полиморфную версию в Applicative:

precise :: (Applicative f) => f (Integer, v) -> TreeMap v -> f (Integer, v)
precise closestSoFar Leaf = closestSoFar
precise closestSoFar (Node k v l r) = case i `compare` k of
  LT -> precise closestSoFar l
  EQ -> pure (k, v)
  GT -> precise (pure (k, v)) r

Само по себе это мало что дает, но если мы знаем, что GTветвь всегда будет возвращать значение, мы можем заставить его работать в Identityфункторе, независимо от начального функтора. То есть мы можем начать с Maybeфунктора, но перейдем к Identityфунктору в GTветке:

closestLess :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess i = precise Nothing
  where
    precise :: (Applicative t) => t (Integer, v) -> TreeMap v -> t (Integer, v)
    precise closestSoFar Leaf = closestSoFar
    precise closestSoFar (Node k v l r) = case i `compare` k of
      LT -> precise closestSoFar l
      EQ -> pure (k, v)
      GT -> pure . runIdentity $ precise (Identity (k, v)) r

Это отлично работает с вашим тестовым примером:

> isJust $ closestLess 5 (Node 3 () Leaf undefined)
True

и является хорошим примером полиморфной рекурсии.

Еще одна приятная особенность этого подхода с точки зрения производительности заключается в том, что он -ddump-simplпоказывает, что оберток или словарей нет. Все это было стерто на уровне типов со специализированными функциями для двух функторов:

closestLess
  = \ @ v i eta ->
      letrec {
        $sprecise
        $sprecise
          = \ @ v1 closestSoFar ds ->
              case ds of {
                Leaf -> closestSoFar;
                Node k v2 l r ->
                  case compareInteger i k of {
                    LT -> $sprecise closestSoFar l;
                    EQ -> (k, v2) `cast` <Co:5>;
                    GT -> $sprecise ((k, v2) `cast` <Co:5>) r
                  }
              }; } in
      letrec {
        $sprecise1
        $sprecise1
          = \ @ v1 closestSoFar ds ->
              case ds of {
                Leaf -> closestSoFar;
                Node k v2 l r ->
                  case compareInteger i k of {
                    LT -> $sprecise1 closestSoFar l;
                    EQ -> Just (k, v2);
                    GT -> Just (($sprecise ((k, v2) `cast` <Co:5>) r) `cast` <Co:4>)
                  }
              }; } in
      $sprecise1 Nothing eta

2
Это довольно крутое решение
Luqui

3

Начиная с моей не ленивой реализации, я сначала рефакторинг, preciseчтобы получить Justв качестве аргумента, и обобщил его тип соответственно:

data TreeMap v = Leaf | Node Integer v (TreeMap v) (TreeMap v) deriving (Show, Read, Eq, Ord)

closestLess :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess i = precise Just Nothing where
  precise :: ((Integer, v) -> t) -> t -> TreeMap v -> t
  precise _ closestSoFar Leaf = closestSoFar
  precise wrap closestSoFar (Node k v l r) = case i `compare` k of
    LT -> precise wrap closestSoFar l
    EQ -> wrap (k, v)
    GT -> precise wrap (wrap (k, v)) r

Затем я изменил это, чтобы сделать wrapрано и вызвать себя idв GTслучае:

data TreeMap v = Leaf | Node Integer v (TreeMap v) (TreeMap v) deriving (Show, Read, Eq, Ord)

closestLess :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess i = precise Just Nothing where
  precise :: ((Integer, v) -> t) -> t -> TreeMap v -> t
  precise _ closestSoFar Leaf = closestSoFar
  precise wrap closestSoFar (Node k v l r) = case i `compare` k of
    LT -> precise wrap closestSoFar l
    EQ -> wrap (k, v)
    GT -> wrap (precise id (k, v) r)

Это по-прежнему работает точно так же, как и раньше, за исключением добавленной лени.


1
Все ли эти idпромежуточные Justи финальные значения (k,v)устранены компилятором? скорее всего, нет, функции должны быть непрозрачными, и вы могли бы (возможно, по типу) использовать их first (1+)вместо того, idчтобы все, что знает компилятор. но это делает для компактного кода ... конечно, мой код - это распутывание и спецификация вашего здесь с дополнительным упрощением (устранение ids). также очень интересно, как более общий тип служит в качестве ограничения, отношения между задействованными значениями (хотя и недостаточно жесткими, с first (1+)разрешением as wrap).
Уилл Несс

1
(продолжение). Ваша полиморфность preciseиспользуется в двух типах, непосредственно соответствующих двум специализированным функциям, используемым в более подробном варианте. там хорошее взаимодействие Кроме того, я бы не назвал этот CPS, wrapон не используется в качестве продолжения, он не создается "изнутри", он складывается - путем рекурсии - снаружи. Может быть , если бы были использованы в качестве продолжения вы могли бы избавиться от этих посторонних idс ... Кстати , мы можем увидеть здесь еще раз , что старый образец функционального аргумента используется как индикатор того , что делать, переключение между двумя курсами действия ( Justили id).
Будет Несс

3

Я думаю, что версия CPS, на которую вы ответили сами, является лучшей, но для полноты изложения приведу еще несколько идей. (РЕДАКТИРОВАТЬ: ответ Бура в настоящее время является наиболее эффективным.)

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

closestLess1 :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess1 _ Leaf = Nothing
closestLess1 i (Node k v l r) =
  case i `compare` k of
    LT -> closestLess1 i l
    EQ -> Just (k, v)
    GT -> Just (fromMaybe (k, v) (closestLess1 i r))

Это проще, но занимает больше места в стеке, когда вы сталкиваетесь с большим количеством GTслучаев. Технически вы могли бы даже использовать это fromMaybeв форме аккумулятора (т. fromJustЕ. Заменить неявное в ответе Луки), но это было бы избыточной, недоступной ветвью.

Другая идея состоит в том, что на самом деле есть две «фазы» алгоритма, одна до и одна после того, как вы нажали a GT, поэтому вы параметризуете его логическим значением для представления этих двух фаз и используете зависимые типы для кодирования инварианта, что всегда будет результат на втором этапе.

data SBool (b :: Bool) where
  STrue :: SBool 'True
  SFalse :: SBool 'False

type family MaybeUnless (b :: Bool) a where
  MaybeUnless 'True a = a
  MaybeUnless 'False a = Maybe a

ret :: SBool b -> a -> MaybeUnless b a
ret SFalse = Just
ret STrue = id

closestLess2 :: Integer -> TreeMap v -> Maybe (Integer, v)
closestLess2 i = precise SFalse Nothing where
  precise :: SBool b -> MaybeUnless b (Integer, v) -> TreeMap v -> MaybeUnless b (Integer, v)
  precise _ closestSoFar Leaf = closestSoFar
  precise b closestSoFar (Node k v l r) = case i `compare` k of
    LT -> precise b closestSoFar l
    EQ -> ret b (k, v)
    GT -> ret b (precise STrue (k, v) r)

Я не думал о своем ответе как о CPS, пока вы не указали его. Я думал о чем-то более близком к трансформации рабочего-обертки. Я думаю, Раймонд Чен снова наносит удар!
Джозеф Сибл-Восстановить Монику

2

Как насчет

GT -> let Just v = precise (Just (k,v) r) in Just v

?


Потому что это неполное совпадение с образцом. Даже если моя функция целая, полная, я не люблю, чтобы ее части были частичными.
Джозеф Сибл-Восстановить Монику

Итак, вы сказали «мы знаем наверняка», все еще с некоторым сомнением. Возможно, это здорово.
Луки

Мы точно знаем, учитывая, что мой второй блок кода в моем вопросе всегда возвращается, Justно является полным. Я знаю, что ваше решение в том виде, в котором оно написано, на самом деле является полным, но оно хрупкое в том смысле, что казалось бы безопасная модификация может привести к его падению.
Джозеф Сибл-Восстановить Монику

Это также немного замедлит работу программы, поскольку GHC не может доказать, что так будет всегда Just, поэтому он добавит тест, чтобы убедиться, что он не Nothingкаждый раз повторяется.
Джозеф Сибл-Восстановить Монику

1

Мало того, что мы всегда знаем Just, после его первого открытия, мы также всегда знаем Nothing до тех пор. Это на самом деле две разные "логики".

Итак, в первую очередь мы идем налево, так что сделайте это явно:

data TreeMap v = Leaf | Node Integer v (TreeMap v) (TreeMap v) 
                 deriving (Show, Read, Eq, Ord)

closestLess :: Integer 
            -> TreeMap v 
            -> Maybe (Integer, v)
closestLess i = goLeft 
  where
  goLeft :: TreeMap v -> Maybe (Integer, v)
  goLeft n@(Node k v l _) = case i `compare` k of
          LT -> goLeft l
          _  -> Just (precise (k, v) n)
  goLeft Leaf = Nothing

  -- no more maybe if we're here
  precise :: (Integer, v) -> TreeMap v -> (Integer, v)
  precise closestSoFar Leaf           = closestSoFar
  precise closestSoFar (Node k v l r) = case i `compare` k of
        LT -> precise closestSoFar l
        EQ -> (k, v)
        GT -> precise (k, v) r

Цена повторяется не более одного шага не более одного раза.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.