我一直在努力学习应用仿函数的静态分析。许多消息来源称,使用它们优于monad的优点是易受静态分析的影响。
但是,我发现实际执行静态分析的唯一example对我来说太复杂了。有没有更简单的例子?
具体来说,我想知道我是否可以对递归应用程序执行静态分析。例如,像:
y = f <$> x <*> y <*> z
在分析上面的代码时,是否可以检测到它在y上是递归的?或者参考透明度是否仍然阻止这种情况发生?
答案 0 :(得分:15)
Applicative仿函数允许在运行时进行静态分析。一个更简单的例子可以更好地解释这一点。
想象一下,您想要计算一个值,但想要跟踪该值具有哪些依赖关系。例如,您可以使用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 a
Num的实例:
instance (Num a) => Num (Input a) where
(+) = liftA2 (+)
(*) = liftA2 (*)
abs = liftA abs
signum = liftA signum
fromInteger = pure . fromInteger
Nexts,让我们做几个输入:
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,我们就无法做到!它将取决于最流行的股票,唯一的方法是运行IO操作。因此,当类型使用Monad时,不可能静态跟踪依赖关系,但只使用Applicative完全可能。这是一个很好的例子,为什么通常较少的功率意味着更有用 - 因为Applicative不允许选择,依赖关系可以静态计算。
编辑:关于检查y
是否递归本身,如果您愿意注释您的函数名称,可以使用applicative functors进行这样的检查。
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]
答案 1 :(得分:3)
是的,应用函子允许比monad更多的分析。但不,你不能观察递归。我写了一篇关于解析的文章,详细解释了这个问题:
https://lirias.kuleuven.be/bitstream/123456789/352570/1/gc-jfp.pdf
然后,本文讨论了一种替代的递归编码,它允许分析,并具有一些其他优点和一些缺点。其他相关工作是:
https://lirias.kuleuven.be/bitstream/123456789/376843/1/p97-devriese.pdf
在这些论文的相关工作部分可以找到更多相关的工作......