haskellhigher-order-functionslinear-types

Lowering of higher order function with linear types


I've been experimenting with linear types recently, and have been wondering if the following transformation is possible. It's definitely not valid without linear types.

The aim is to lower the higher order function argument. This should be ok, because linear types ensure the HOF is called exactly once, so exactly 1 value of a exists. The issue is how to escape the lambda and observe a

   lower :: ((a %1-> b) %1-> r) %1-> b %1-> (r,a)

Solution

  • Edit: You cannot safely implement lower. As dfeuer and danidiaz say in comments: what if the first argument to lower is the identity function? With the implementation I showed in my old answer below you can write:

    main :: IO ()
    main = putStrLn (snd (lower (\x -> x) False))
    

    Which will produce an error at run time:

    Lin: Prelude.undefined
    CallStack (from HasCallStack):
      error, called at libraries/base/GHC/Err.hs:74:14 in base:GHC.Err
      undefined, called at Lin.hs:25:19 in main:Main
    

    So linear types do not give enough guarantees to make it safe to implement this function.

    Edit2: You can save the situation by slightly changing the signature to: lower :: ((a %1 -> b) %1 -> Ur r) %1 -> b %1 -> (Ur r, a). This way it is impossible to store the linear argument a %1 -> b in r.

    import Control.Exception (evaluate)
    
    -- from Data.Unrestricted.Linear from linear-base
    data Ur a where
      Ur :: a -> Ur a
    
    lower :: ((a %1 -> b) %1 -> Ur r) %1 -> b %1 -> (Ur r, a)
    lower = toLinear2 lower'
    
    lower' :: ((a %1 -> b) %1 -> Ur r) -> b -> (Ur r, a)
    lower' f b = unsafePerformIO $ do
      ref <- newIORef undefined
      r <- evaluate $ f (toLinear (\a -> unsafePerformIO $ b <$ writeIORef ref a))
      a <- readIORef ref
      pure (r, a)
    

    Old answer

    This may not be the answer you are looking for, but if you know for sure that it is safe, then you can bend the rules a bit:

    {-# LANGUAGE LinearTypes, GADTs #-}
    import System.IO.Unsafe
    import Data.IORef
    import Unsafe.Coerce
    import Control.Exception (evaluate)
    
    -- 'coerce', 'toLinear' and 'toLinear2' are also found in Unsafe.Linear from linear-base
    
    coerce :: forall a b. a %1-> b
    coerce a =
      case unsafeEqualityProof @a @b of
        UnsafeRefl -> a
    {-# INLINE coerce #-}
    
    toLinear :: (a %p-> b) %1-> (a %1-> b)
    toLinear = coerce
    
    toLinear2 :: (a %p-> b %q-> c) %1-> (a %1-> b %1-> c)
    toLinear2 = coerce
    
    lower :: ((a %1 -> b) %1 -> r) %1 -> b %1 -> (r, a)
    lower = toLinear2 lower'
    
    lower' :: ((a %1 -> b) %1 -> r) -> b -> (r, a)
    lower' f b = unsafePerformIO $ do
      ref <- newIORef undefined
      r <- evaluate $ f (toLinear (\a -> unsafePerformIO $ b <$ writeIORef ref a))
      a <- readIORef ref
      pure (r, a)
    

    I think the main purpose of linear types is only to get the extra type-level information, so I don't think there are any safe primitives that let you do this more cleanly (although I don't know all the ins and outs). Linear types do allow you to implement a safe interface with unsafe operations underneath. See for example how many times toLinear is used in this file from linear-base.

    Perhaps the unsafe bits above can be outsourced to a lower-level library. Maybe something like promises, but then with linear types.