6

I'm having a hard time understanding the documentation/examples out there describing the reflection package. I'm an imperative-programming veteran but a Haskell newb. Can you walk me through a very simple introduction?

Package: https://hackage.haskell.org/package/reflection

Edit: to whoever closed this question: this is meant to be a beginner introduction to Haskell reflection. The answer below is excellent and others would be useful too, so please reopen.

AgentLiquid
  • 3,632
  • 7
  • 26
  • 30
  • 3
    Does the tutorial linked on the haddock page help: https://www.schoolofhaskell.com/user/thoughtpolice/using-reflection ? – Sibi Nov 17 '20 at 09:28
  • Related: https://stackoverflow.com/a/29929718/1364288 https://stackoverflow.com/a/28651952/1364288 Note that "reflection" is an advanced technique that relies for its current implementation in a peculiarity of GHC's inner workings. It's not a technique for beginner/intermediate users. – danidiaz Nov 17 '20 at 12:06

1 Answers1

7

In the simplest use-case, if you have some configuration information you'd like to make generally available across a set of functions:

data Config = Config { w :: Int, s :: String }

you can add a Given Config constraint to the functions that need access to the configuration:

timesW :: (Given Config) => Int -> Int

and then use the value given to refer to the current configuration (and so w given or s given to refer to its fields):

timesW x = w given * x

With a few other functions, some that use the configuration and some that don't:

copies :: Int -> String -> String
copies n str = concat (replicate n str)

foo :: (Given Config) => Int -> String
foo n = copies (timesW n) (s given)

you can then run a computation under different configurations that you give:

main = do
  print $ give (Config 5 "x") $ foo 3
  print $ give (Config 2 "no") $ foo 4

This is similar to:

  • defining given :: Config globally, except you can run the computation under multiple configurations in the same program;

  • passing the configuration around as an extra parameter to every function, except you avoid the bother of explicitly accepting the configuration and passing it on, like:

    timesW cfg x = w cfg * x
    foo cfg n = copies (timesW cfg n) (s cfg)
    
  • using a Reader monad, but you don't have to lift everything to an awkward monad- or applicative-level syntax, like:

    timesW x = (*) <$> asks w <*> pure x
    foo n = copies <$> timesW n <*> asks s
    

The full example:

{-# LANGUAGE FlexibleContexts #-}

import Data.Reflection

data Config = Config { w :: Int, s :: String }

timesW :: (Given Config) => Int -> Int
timesW x = w given * x

copies :: Int -> String -> String
copies n str = concat (replicate n str)

foo :: (Given Config) => Int -> String
foo n = copies (timesW n) (s given)

main = do
  print $ give (Config 5 "x") $ foo 3
  print $ give (Config 2 "no") $ foo 4
K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • This is excellent, thank you for taking the time. It really feels like another way of doing implicit parameters, correct? Why call it "reflection" exactly? It doesn't line up with my previous intuition of that word. – AgentLiquid Nov 17 '20 at 19:29
  • 1
    If you look at the original article ([Kiselyov and Shan, "Functional Pearl: Implicit Configurations"](http://okmij.org/ftp/Haskell/tr-15-04.pdf)), they compare it to implicit parameters in Section 6.2. The term "reflection" refers to the method of implementation: the run-time configuration values are "reflected" from the value level to the type level. Usually in programming, "reflection" refers to reflection in the opposite direction, from the type level to the value level (e.g., reflection in Java, say), but it's the same idea. – K. A. Buhr Nov 17 '20 at 19:48
  • 1
    Actually, technically the `reflect` function provided by the `reflection` package takes a type-level argument and returns its value-level value, while `reify` is used to take a value-level value to the type-level, so maybe the intended meaning of "reflection" here is actually in the usual type-level-to-value-level direction. – K. A. Buhr Nov 17 '20 at 19:57
  • Hm, I'm not sure the comparisons in Section 6.2 of the original article apply to the `reflection` package. In particular, the article uses an `ST`-style phantom parameter to allow multiple `Given`s of the same type without fear of using the wrong one at the wrong time. Several of the comparison points stem from this disambiguation feature -- possibly all, it's not clear to me. – Daniel Wagner Nov 18 '20 at 03:59
  • @DanielWagner, `reflection` also provides the `ST`-style phantom version via `Reifies` in place of `Given`, so I think the comparison would be relevant to the more complex `Reifies` interface, if I'm understanding correctly. – K. A. Buhr Nov 18 '20 at 16:00
  • 1
    That's not really how `Given` and `give` should be used. `Given` is best seen as a mechanism for working with *singleton* types, where there's only one value of the type (or sometimes where there are multiple values but you never care which). `Reifies` and `reify` are for configurations. – dfeuer Nov 19 '20 at 02:10