Анализ аппликативных функторов
Я пытался узнать о статическом анализе аппликативных функторов. Многие источники говорят, что преимуществом их использования по сравнению с монадами является склонность к статическому анализу.
Однако единственный пример, который я могу найти для выполнения статического анализа, слишком сложен для меня, чтобы понять. Есть ли более простые примеры этого?
В частности, я хочу знать, могу ли я выполнять статический анализ рекурсивных приложений. Например, что-то вроде:
y = f <$> x <*> y <*> z
Анализируя приведенный выше код, можно ли обнаружить, что он рекурсивен по y? Или ссылочная прозрачность все еще не позволяет сделать это возможным?
2 ответа
Аппликативные функторы позволяют проводить статический анализ во время выполнения. Это лучше объяснить на более простом примере.
Представьте, что вы хотите вычислить значение, но хотите отслеживать зависимости этого значения. Например, вы можете использовать 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
Функция просто вернет значение без каких-либо зависимостей. <*>
combiner объединит два списка зависимостей (удалив дубликаты) и объединит два действия:
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
может выразить выбор, поскольку одно действие может определить, каким будет следующее действие.
Представь Monad
интерфейс для Input
и у вас был следующий код:
mostPopularStock :: Input String
mostPopularStock = Input { dependencies ["Popular Stock"], getInput = readFromWebMostPopularStock }
newPortfolio = do
stock <- mostPopularStock
stockPriceOf "Apple" * 40 + stockPriceOf stock * 10
Теперь, как мы можем рассчитать зависимости newPortolio
? Оказывается, мы не можем сделать это без использования IO! Это будет зависеть от самой популярной акции, и единственный способ узнать это - запустить действие ввода-вывода. Поэтому невозможно статически отслеживать зависимости, когда тип использует Monad, но полностью возможно только с Applicative. Это хороший пример того, почему зачастую меньшая мощность означает большую полезность - поскольку Applicative не допускает выбора, зависимости можно рассчитывать статически.
Изменить: Что касается проверки, если 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]
Да, аппликативные функторы позволяют проводить больший анализ, чем монады. Но нет, вы не можете наблюдать рекурсию. Я написал статью о разборе, которая подробно объясняет проблему:
https://lirias.kuleuven.be/bitstream/123456789/352570/1/gc-jfp.pdf
Затем в документе обсуждается альтернативное кодирование рекурсии, которое позволяет проводить анализ и имеет некоторые другие преимущества и некоторые недостатки. Другая связанная работа:
https://lirias.kuleuven.be/bitstream/123456789/376843/1/p97-devriese.pdf
И больше связанной работы можно найти в соответствующих разделах работы этих документов...