Can some explain the reflection package API in *very* simple terms?
Asked Answered
R

1

7

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.

Romain answered 17/11, 2020 at 7:28 Comment(2)
Does the tutorial linked on the haddock page help: schoolofhaskell.com/user/thoughtpolice/using-reflection ?Peer
Related: https://mcmap.net/q/1447501/-dynamically-generate-haskell-types-at-runtime https://mcmap.net/q/1479657/-function-from-mappend-function-to-monoid-instance 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.Consubstantiation
N
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
Nathanson answered 17/11, 2020 at 17:1 Comment(6)
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.Romain
If you look at the original article (Kiselyov and Shan, "Functional Pearl: Implicit Configurations"), 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.Nathanson
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.Nathanson
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 Givens 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.Ruffi
@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.Nathanson
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.Crowned

© 2022 - 2024 — McMap. All rights reserved.