سؤال

I've been trying to learn about static analysis of applicative functors. Many sources say that an advantage of using them over monads is the susceptibility to static analysis.

However, the only example I can find of actually performing static analysis is too complicated for me to understand. Are there any simpler examples of this?

Specifically, I want to know if I can performing static analysis on recursive applications. For example, something like:

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

When analyzing the above code, is it possible to detect that it is recursive on y? Or does referential transparency still prevent this from being possible?

هل كانت مفيدة؟

المحلول

Applicative functors allow static analysis at runtime. This is better explained by a simpler example.

Imagine you want to calculate a value, but want to track what dependencies that value has. Eg you may use IO a to calculate the value, and have a list of Strings for the dependencies:

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

Now we can easily make this an instance of Functor and Applicative. The functor instance is trivial. As it doesn't introduce any new dependencies, you just need to map over the runInput value:

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

The Applicative instance is more complicated. the pure function will just return a value with no dependencies. The <*> combiner will concat the two list of dependencies (removing duplicates), and combine the two actions:

instance Applicative Input where
  pure = Input [] . return

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

With that, we can also make an Input a an instance of Num if Num a:

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

Nexts, lets make a couple of Inputs:

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

Finally, lets make a value that uses some inputs:

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

This is a pretty cool value. Firstly, we can find the dependencies of portfolioValue as a pure value:

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

That is the static analysis that Applicative allows - we know the dependencies without having to execute the action.

We can still get the value of the action though:

> runInput portfolioValue >>= print
5400.0

Now, why can't we do the same with Monad? The reason is Monad can express choice, in that one action can determine what the next action will be.

Imagine there was a Monad interface for Input, and you had the following code:

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

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

Now, how can we calculate the dependencies of newPortolio? It turns out we can't do it without using IO! It will depend on the most popular stock, and the only way to know is to run the IO action. Therefore it isn't possible to statically track dependencies when the type uses Monad, but completely possible with just Applicative. This is a good example of why often less power means more useful - as Applicative doesn't allow choice, dependencies can be calculated statically.


Edit: With regards to the checking if y is recursive on itself, such a check is possible with applicative functors if you are willing to annotate your function names.

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]

نصائح أخرى

Yes, applicative functors allow more analysis than monads. But no, you can't observe the recursion. I've written a paper about parsing which explains the problem in detail:

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

The paper then discusses an alternative encoding of recursion which does allow analysis and has some other advantages and some downsides. Other related work is:

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

And more related work can be found in the related work sections of those papers...

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top