Анализ аппликативных функторов

Я пытался узнать о статическом анализе аппликативных функторов. Многие источники говорят, что преимуществом использования их над монадами является восприимчивость к статическому анализу.

Тем не менее, единственный пример, который я могу найти, фактически выполняет статический анализ, слишком сложный для меня, чтобы понять. Есть ли более простые примеры этого?

В частности, я хочу знать, могу ли я выполнять статический анализ для рекурсивных приложений. Например, что-то вроде:

y = f <$> x <*> y <*> z

При анализе вышеуказанного кода можно ли обнаружить, что оно рекурсивно по y? Или ссылочная прозрачность все еще мешает этому быть возможным?

Ответы

Ответ 1

Аппликативные функторы позволяют проводить статический анализ во время выполнения. Это лучше объясняется более простым примером.

Представьте, что вы хотите вычислить значение, но хотите отслеживать зависимости, которые имеет значение. Например, вы можете использовать IO a для вычисления значения и иметь список Strings для зависимостей:

data Input a = Input { dependencies :: [String], runInput :: IO a }

Теперь мы можем легко сделать это экземпляром Functor и Applicative. Экземпляр функтора тривиален. Поскольку он не вводит никаких новых зависимостей, вам просто нужно сопоставить значение runInput:

instance Functor (Input) where
  fmap f (Input deps runInput) = Input deps (fmap f runInput)

Экземпляр Applicative более сложный. функция pure просто вернет значение без зависимостей. Комбайнер <*> объединяет два списка зависимостей (удаление дубликатов) и объединяет два действия:

instance Applicative Input where
  pure = Input [] . return

  (Input deps1 getF) <*> (Input deps2 runInput) = Input (nub $ deps1 ++ deps2) (getF <*> runInput)

С этим мы также можем сделать Input a экземпляр Num, если Num a:

instance (Num a) => Num (Input a) where
  (+) = liftA2 (+)
  (*) = liftA2 (*)
  abs = liftA abs
  signum = liftA signum
  fromInteger = pure . fromInteger

Далее, давайте сделаем пару входов:

getTime :: Input UTCTime
getTime = Input { dependencies = ["Time"], runInput = getCurrentTime }

-- | Ideally this would fetch it from somewhere
stockPriceOf :: String -> Input Double
stockPriceOf stock = Input { dependencies = ["Stock ( " ++ stock ++ " )"], runInput = action } where
  action = case stock of
    "Apple" -> return 500
    "Toyota" -> return 20

Наконец, давайте сделаем значение, которое использует некоторые входы:

portfolioValue :: Input Double
portfolioValue = stockPriceOf "Apple" * 10 + stockPriceOf "Toyota" * 20

Это довольно крутое значение. Во-первых, мы можем найти зависимости portfolioValue как чистое значение:

> :t dependencies portfolioValue
dependencies portfolioValue :: [String]
> dependencies portfolioValue
["Stock ( Apple )","Stock ( Toyota )"]

Это статический анализ, который позволяет Applicative - мы знаем зависимости без необходимости выполнять действие.

Мы все равно можем получить значение действия:

> runInput portfolioValue >>= print
5400.0

Теперь, почему мы не можем сделать то же самое с Monad? Причина в том, что Monad может выразить выбор, поскольку одно действие может определить, каким будет следующее действие.

Представьте, что для Input был Monad интерфейс, и у вас был следующий код:

mostPopularStock :: Input String
mostPopularStock = Input { dependencies ["Popular Stock"], getInput = readFromWebMostPopularStock }

newPortfolio = do
  stock <- mostPopularStock
  stockPriceOf "Apple" * 40 + stockPriceOf stock * 10

Теперь, как мы можем вычислить зависимости newPortolio? Оказывается, мы не можем сделать это, не используя IO! Это будет зависеть от самого популярного запаса, и единственный способ узнать - запустить действие IO. Поэтому невозможно статически отслеживать зависимости, когда тип использует Monad, но полностью возможен с помощью только аппликативного. Это хороший пример того, почему часто меньшая мощность означает более полезную, поскольку Аппликатив не позволяет выбирать, зависимости могут быть рассчитаны статически.


Изменить: Что касается проверки того, является ли y рекурсивным сам по себе, такая проверка возможна с аппликативными функторами, если вы хотите аннотировать имена ваших функций.

data TrackedComp a = TrackedComp { deps :: [String],  recursive :: Bool, run :: a}

instance (Show a) => Show (TrackedComp a) where
  show comp = "TrackedComp " ++ show (run comp)

instance Functor (TrackedComp) where
  fmap f (TrackedComp deps rec1 run) = TrackedComp deps rec1 (f run)

instance Applicative TrackedComp where
  pure = TrackedComp [] False

  (TrackedComp deps1 rec1 getF) <*> (TrackedComp deps2 rec2 value) =
    TrackedComp (combine deps1 deps2) (rec1 || rec2) (getF value)

-- | combine [1,1,1] [2,2,2] = [1,2,1,2,1,2]
combine :: [a] -> [a] -> [a]
combine x [] = x
combine [] y = y
combine (x:xs) (y:ys) = x : y : combine xs ys

instance (Num a) => Num (TrackedComp a) where
  (+) = liftA2 (+)
  (*) = liftA2 (*)
  abs = liftA abs
  signum = liftA signum
  fromInteger = pure . fromInteger

newComp :: String -> TrackedComp a -> TrackedComp a
newComp name tracked = TrackedComp (name : deps tracked) isRecursive (run tracked) where
   isRecursive = (name `elem` deps tracked) || recursive tracked 


y :: TrackedComp [Int]
y = newComp "y" $ liftA2 (:) x z

x :: TrackedComp Int
x = newComp "x" $ 38

z :: TrackedComp [Int]
z = newComp "z" $ liftA2 (:) 3 y

> recursive x
False
> recursive y
True
> take 10 $ run y
[38,3,38,3,38,3,38,3,38,3]

Ответ 2

Да, аппликативные функции позволяют провести больше анализа, чем монады. Но нет, вы не можете наблюдать рекурсию. Я написал статью о разборе, которая подробно объясняет проблему:

https://lirias.kuleuven.be/bitstream/123456789/352570/1/gc-jfp.pdf

Далее в документе рассматривается альтернативное кодирование рекурсии, которое позволяет анализировать и имеет некоторые другие преимущества и некоторые недостатки. Другие связанные работы:

https://lirias.kuleuven.be/bitstream/123456789/376843/1/p97-devriese.pdf

И более связанные работы можно найти в соответствующих разделах работы этих работ...