Add “context fixes” to the language. #40

Open
wants to merge 12 commits into
from

Projects

None yet
@nomeata
Contributor
nomeata commented Jan 20, 2017
@parsonsmatt

How does usage look? eg given:

context fixes progName in
  foo :: Maybe Int -> Either String Int
  foo Nothing  = Left $ progName ++ ": no number given"
  foo (Just i) = bar i

  bar :: Int -> Either String Int
  bar 0 = Left $ progName ++ ": zero no good"
  bar n = Right $ n + 1

what does this look like in the export list? In the import list? How do you use bar with the implicit parameter, and what is it's type?

proposals/0000-context-fixes.rst
+
+* Besides the indentation and replacing ``42`` by ``n``, I did not have to change any code.
+* It is very obvious to the reader that wihtin the indentet block, ``n`` is not changed.
+* The typo of ``foo`` is different within the scope of the ``context`` block: It is ``T -> S`` inside, but ``Int -> T -> S`` outside.
@markus2330
markus2330 Jan 20, 2017

Some typos in this paragraph: wihtin -> within, indentet -> indented, typo -> type?

And I would say "obvious" is enough 😄

Some links to the background where you borrowed the syntax from would be useful (according to your Debian blog post Isabelle?).

What about going from "read only once at startup" to "read at configuration reloads"?

@jberryman
jberryman commented Jan 20, 2017 edited

Very cool. If anything I think you're over-generous in your alternatives section; I'd find e.g. the Reader only preferable when you've also got a type synonym for your monad stack you're using, and you'd be threading it through say 3 or more functions

What about something like the following syntax to make it clear that we're sort of closed over these names within the block:

context :: String -> OtherStuff ->
context progName otherStuff =
  foo :: Maybe Int -> Either String Int
  foo Nothing  = Left $ progName ++ ": no number given"
  foo (Just i) = bar (i, otherStuff)
  -- etc.

so context is a keyword here still. No idea if that's parseable but maybe something similar would be nice.

EDIT: or maybe top level lambda?

\progName (otherStuff :: OtherStuffNeedsSignature) ->
  foo :: Maybe Int -> Either String Int
  foo Nothing  = Left $ progName ++ ": no number given"
  foo (Just i) = bar (i, otherStuff)
  -- etc.
@jberryman

Is the idea that this should be only at the top level (is that a topdecl)? If so I think the proposal should be expanded so that these can be nested, etc. anywhere we allow declarations.

Actually, that's sort of silly because in where clauses we can simply add e.g. progName = foo and have it be in scope in its neighbor clauses. Maybe that observation could suggest another alternative syntax...?

Anyway, I also wanted to say that this is something I could see myself reaching for even during a small refactor, and I have a use-case I ran into just a few weeks ago where I evaluated and dismissed all the alternatives you mentioned (nothing interesting, just needing to thread a new parameter through a bunch of parsing functions).

@jberryman

@parsonsmatt I got the idea that

context fixes progName in
  foo :: Maybe Int -> Either String Int
  foo Nothing  = Left $ progName ++ ": no number give

simply desugars to

foo :: String -> Maybe Int -> Either String Int
foo progName Nothing  = Left $ progName ++ ": no number given"

but agree it's not totally clear from the proposal

@nomeata nomeata Incorporate suggestions by @markus2330
in particular, section on related work.
fc735a3
@nomeata
Contributor
nomeata commented Jan 20, 2017

The syntax proposed by @jberryman is worth considering. Maybe in the variant

context :: String -> OtherStuff -> …
context progName otherStuff =
  foo :: Maybe Int -> Either String Int
  foo Nothing  = Left $ progName ++ ": no number given"
  foo (Just i) = bar (i, otherStuff)
  -- etc.

where is a keyword as well. Just to make it look less incomplete, and easier to spot.

But having the context … in frame allows for future extensions beyond fixes, which is also worth while.

@jberryman

Here's the shape of the real-life refactoring I was thinking of. The only interesting things here are 1) that we have two different values that need to be threaded to bottom, and 2) that reused is called with each of the two values

toplevel = 
  ParsedThing <$> foo <*> reused <*> bar <*> baz

foo = X <$> reused <*> ...
reused = ... bottom ...
bar = ... bottom ...
baz = ... bottom ..
bottom = ...

refactored to thread new throughout to make it available to bottom which is the only function actually using it.

toplevel =
  let (new1,new2) = getNewStuffWeDidntNeedBefore
   in ParsedThing <$> foo new1 <*> reused new2 <*> bar new2 <*> baz new1

foo new = X <$> reused new <*> ...
reused new1_or_2 = ... bottom new1_or_2 ...
bar new = ... bottom new ...
baz new = ... bottom new ...
bottom new = doStuffWith new

Haven't thought about what this looks like under the proposal yet, so take my comment #40 (comment) with a grain of salt

@nomeata
Contributor
nomeata commented Jan 20, 2017
context new where
  foo = X <$> reused <*> ...
  reused = ... bottom ...
  bar = ... bottom ...
  baz = ... bottom ..
  bottom = doStuffWith new

should do exactly what you want.

@hsenag
hsenag commented Jan 20, 2017 edited

It's a fairly subjective view, but I have a lot of trouble intuitively understanding the phrase "context fixes". I don't have a good alternative though.

@nomeata
Contributor
nomeata commented Jan 20, 2017

“In this context, progName is fixed”. Or “This context fixes the parameter progName”. It’s taken from Isabelle, so I guess it feels more natural to me.

@siddhanathan

I feel like this is unnecessary syntax. One of my gripes while learning Haskell was the availability of both let and where syntax. This adds more syntax newcomers have to learn.

For the motivating examples, you could simply just use a let binding, and if the let binding shouldn't be accessible to the entire file, either your file is too big, or you should use a where clause to group related bindings together. More complicated examples can use the Reader monad.

Is there a case where this allows more succinct code than just a combination of let and where?

@nomeata
Contributor
nomeata commented Jan 21, 2017

A let or where can only exists within one function; they cannot cover multiple top-level functions as context does. So this is not a valid counter-argument.

@siddhanathan

Sorry, by let I meant variable assignment. Something like this should work:

main = putStrLn $ go1 "Hello" ++ go2 "World"
  where
    go1 :: String -> String
    go1 = concat . replicate n

    n :: Int
    n = 5

    go2 :: String -> String
    go2 = concat . replicate n

The value n is not accessible outside the where block, so it's in a particular context. You can also nest where blocks for more fine grained control.

@nomeata
Contributor
nomeata commented Jan 21, 2017

That works great if in my one of the functions (here main) is meant to be externally visible. My proposal aims to bring the same expressitivity to the case when you need to do this to multiple functions that are meant to be externally visible.

@simonpj
simonpj commented Jan 23, 2017

I'm not keen on this proposal. It adds convenience, perhaps, in certain particular situations, but it adds no expressiveness. Backpack would give you all this convenience along with efficiency. (Backpack is of course MUCH more elaborate, but it also does a lot more.)

By way of analogy, I thought that associated types (where we declare a type function in a class declaration rather that at top level) would be an easy win; but actually having type family declarations nested inside something else, and ditto instances, has turned out to be really quite painful in implementation terms. In the case of associated types there is a real win, but I still wonder. The present proposal seems much less compelling to me.

There are always unexpected feature interactions.

So I'm not keen.

@simonmar

I'm also not keen. I don't think this extension pays its way: Haskell is already a large language, and there are many ways to do simple things, which is daunting for beginners. The bar should be high for new syntactic sugar, and for me this proposal falls beneath it.

@nomeata
Contributor
nomeata commented Jan 23, 2017

Backpack would give you all this convenience along with efficiency.

I highly doubt that. Backpack allows me to abstract code over static parts of it, but not over dynamic, but locally fixed. Edward confirms this on reddit:

Not actually! Backpack is specifically designed to NOT support runtime module selection.

@nomeata
Contributor
nomeata commented Jan 23, 2017 edited

I don't think this extension pays its way

Even when you see it as an alternative to implicit parameters? Both proposal solve a similar problem, very differently, and this one seems to be cleaner and more straight-forward than implicit parameters. So it “pays” its way by reducing the need for implicit parameters.

@simonmar

Even when you see it as an alternative to implicit parameters?

I think a better alternative to implicit parameters is the reflection package.

Something I find a bit unsettling about this proposal is the magic extra parameter, whereas with reflection it's visible in the type (good) and you don't have to pass it explicitly (also good). I'm really starting to like reflection.

@nomeata
Contributor
nomeata commented Jan 23, 2017

Something I find a bit unsettling about this proposal is the magic extra parameter, whereas with reflection it's visible in the type (good) and you don't have to pass it explicitly (also good). I'm really starting to like reflection.

It no more magic or more invisible than conf in the following code:

foo conf = … bar … 
  where
     bar :: Int -> Int
     bar n = n + inc conf

Note that the fact that bar refers to conf is not visible in the type of bar.

About the reflection package: With reify and reflect, you still have to pass the corresponding Proxy s around, so you do not gain the desired advantage! (Unless I misread the API.)

Or you use give and given, but it warns “If multiple instances are in scope, then the behavior is implementation defined.” – not very appealing.

@Icelandjack

Are other constructs allowed in contexts?

Type class declarations + instances, ..

@nomeata
Contributor
nomeata commented Jan 23, 2017

Are other constructs allowed in contexts?

Type class declarations + instances, ..

No, only decls, but not topdecls, as defined in the report. So in particular no type classes or instances, or new types. This restriction corresponds to what Isabelle does here, and is unavoidable without dependent types.

@simonmar

It no more magic or more invisible than conf in the following code:

 foo conf = … bar … 
   where
      bar :: Int -> Int
      bar n = n + inc conf

The difference is that here you don't have to pass an argument to bar that doesn't appear in its (declared) type, whereas in this proposal you do - that's what I find unsettling.

About the reflection package: With reify and reflect, you still have to pass the corresponding Proxy s around, so you do not gain the desired advantage! (Unless I misread the API.)

In fact you don't have to pass around the proxy, you can make a new one any time. The API is a bit strange in giving you a proxy value that you can just discard, but I'm sure @ekmett had good reasons.

Or you use give and given, but it warns “If multiple instances are in scope, then the behavior is implementation defined.” – not very appealing.

I'm actually not sure what this refers to, because I can't figure out how you'd get multiple instances in scope. But this aside, the API is really simple and does exactly what you want.

@nomeata
Contributor
nomeata commented Jan 23, 2017 edited

The difference is that here you don't have to pass an argument to bar that doesn't appear in its (declared) type, whereas in this proposal you do - that's what I find unsettling.

Within the context I am not passing the argument – the desugared code does, but the same could be said about bar, as after closure conversion it does receive conf as an argument.

So you are concerned that calls from outside the context have to pass the fixed parameters as arguments?

I can see that this is a bit discomforting, and I acknowledge that as a possible weak point of this feature.

On the other hand, it can be seen as adding elegance – the other approaches (Implicit Parameters, reflection) do affect the types in a way that you usually do not want to expose to the user, so you would have to write a wrapper function for every function exposed from your context. With context fixes, that is not necessary – it is a completely local feature that does not leak to the outside of the module.

@ocharles

In fact you don't have to pass around the proxy, you can make a new one any time. The API is a bit strange in giving you a proxy value that you can just discard, ...

It has, to otherwise you have ambiguity:

{-# LANGUAGE RankNTypes, FunctionalDependencies #-}
class Reifies s a | s -> a where reflect :: proxy s -> a
reify :: a -> (forall s. Reifies s a => r) -> r
reify v f = undefined

Won't type check, because nothing specifies what s is. reify :: a -> (forall s. Reifies s a => Tagged s r) -> r could work though.

@mchakravarty
mchakravarty commented Jan 24, 2017 edited

Like @simonmar & @simonpj, I am not keen on this for the reasons they put forward. However, I have another reason as well. Implicit context increases cognitive load and runs counter to much of the clarity gained by FP over, say, imperative programming.

Yes, sure, writing it out is more verbose. That is a good thing. The verbosity means that you can reason locally about the code without needing to know some magic context.

If you add that context, then, yes, you have to touch a lot of code and that is a good reason for getting some decent refactoring tools for Haskell, but I don't think a language extension is the right fix.

Edit: Using Rich Hickey's terminology, this proposal makes things easy, but not simple.

@Roxxik
Roxxik commented Jan 24, 2017

isn't this feature pretty much the same as Idris' parameters keyword? just differently named

http://docs.idris-lang.org/en/latest/tutorial/modules.html#parameterised-blocks

@nomeata
Contributor
nomeata commented Jan 24, 2017

The verbosity means that you can reason locally about the code without needing to know some magic context.

I agree with the notion, but not that it applies here. The cognitive load decreases because as you read the code, you know upfront that conf is going to be the same value everywhere in the block. This is a guarantee that you do not have with implicit or explicit parameters, and this guarantee makes this more functional and less imperative than passing conf around, when we can modify it.

This way of thinking that is something we do everytime when we have a larger where clause
Why is it totally fine and normal to put functions into a where clause, to have them scoped over by the parameters of the enclosing function, but not ok to put functions into a context clause, to have them scoped over by the parameters of the context?

Maybe the proposal is not clear in the fact that calls within the context cannot pass new values for the parameters?

I originally wrote this with a “let’s see what happens attitude“, but the more I have to defend my proposal, the more I start to like it…

isn't this feature pretty much the same as Idris' parameters keyword? just differently named

Thanks for the reference! Very good point. Will add it to the proposal.

@rrnewton

@nomeata, in the extended example, you expanded the argument set of functions like stepTo to include AnimationRate which they previously did not, correct? This makes me wonder about related issues:

  • what if some functions have more constant params than others? The context form does not nest. (But it looks like the Idris one does.)
  • what if we need to "escape" the convention at some call sites?
  • what type should IDEs show for each function? (i.e. the "internal" or "external" type) This has to be sorted if the IDE is to automatically insert the signature for a function, like intero does.

I do think we have some unsolved ergonomics problems with purely functional programming and Haskell in particular. FWIW being able to reverse-search to the enclosing "context" block would be easier than finding the free variables of non-top-level functions. (It's not supported in any Haskell IDE I'm aware of, but it would be great to hover on a function def and highlight all its free variables, ideally with arrows to their binders, like DrRacket.)

That brings me to my main point, which is that this seems like an area for IDE innovation rather than language changes. This is an issue of presentation. Many IDEs hide or fold code. I'd love it if I could put a pragma in a comment that tells the IDE that a certain prefix of arguments should remain constant over a given scope. That would be a hint to hide all those arguments and types by default (e.g. collapse to ...), and it could also be a warning if you try to actually change one of those arguments (i.e. providing anything other than a variable reference to the formal parameter).

The warning is key, we don't want to visually scan all call sites to be realize that a parameter is constant. (I.e. @nomeata's point about the non-implicit parameter being "more imperative" -- allowing the possibility of change.) The Reader monad addresses that of course. Also valuable in an IDE would be a bullet-proof way to refactor pure code into monadic, to inject a Reader monad...

P.S. I partially agree with @mchakravarty about explicit/verbose being a good thing. But I think that holds only up to a point. For example, in our HtDP-based introductory course, we teach students to use functional records only when building world-states for animations and games. But we don't give them lenses or good functional update syntax -- so they end up writing enormous expressions that amount to "nothing changed". It's just line noise and it makes the functional approach seem silly and the imperative one comparatively sensible.

@nomeata
Contributor
nomeata commented Jan 24, 2017

Thanks for your questions!

what if some functions have more constant params than others? The context form does not nest. (But it looks like the Idris one does.)

Sure it does! It can go wherever a decl is valid, even inside a context

context fixes (param1 :: Int) in
  foo :: String -- has type Int -> String outside the outer context
  context fixes (param2 :: Bool) in
    bar :: String -- has type Bool -> String outside the inner and inside the outer context
                  -- has type Int -> Bool -> String outside the outer context

what if we need to "escape" the convention at some call sites?

What precisely do you mean here? If you really do want to call another function in the same scope with a changed argument, you’d have to use a detour via a helper function outside the scope. A bit convoluted, but maybe rightly so – that’s what you get for declaring: “This parameter is locally fixed” without sticking to it.

what type should IDEs show for each function? (i.e. the "internal" or "external" type) This has to be sorted if the IDE is to automatically insert the signature for a function, like intero does.

Good question. I don’t use IDEs :-). Can someone comment what it does in Agda or Idris?

The Reader monad addresses that of course.

Not it doesn’t! The type has a method local :: MonadReader r m => (r -> r) -> m a -> m a.

@int-index

We already have a way to share a common parameter between multiple definitions — where clauses.

foo x = y
  where
    -- definitions here have access to "x"
    -- but are visible only within "foo"
    ...

However, definitions inside where don't escape to the top level. The goal of this proposal can be achieved if we allow them to do so, introducing a where public clause:

foo x = y
  where public
    -- definitions here have access to "x"
    -- and float to top-level
    ...
@goldfirere
Contributor

@int-index , how does your where public differ from the original proposal, other than in syntax? It would still seem that any bindings floated out from a where public need to capture bound variables in the outer definition.

To be fair, I almost wrote a similar counter-proposal, but decided I didn't like the feature because it doesn't scale very well. Haskell allows where to be potentially deeply nested. What would where public mean if buried, say, in the middle of a do block? My only interpretation is that every lambda bound variable (including lambdas that arise from desugaring do binds) would have to get captured. And it would also be nice to have a where where some definitions are public and some are not. But I don't want to think up a concrete syntax for that!

So while I see where public as cleaner somehow than a new top-level declaration, I think it adds quite a bit of complexity.

As regards the initial proposal, I will quote @nomeata himself (though on another ticket): I see the itch you are trying to scratch, but the solution is not very convincing yet.

@int-index

how does your where public differ from the original proposal, other than in syntax?

Oh, it's not meant to differ, it's a reformulation that's supposed to look/feel more Haskell-y and give more intuition to the reader unfamiliar with the feature about what's going on.

Good points about the do-notation and per-definition public/private-control. They apply regardless of the syntax choice, don't they? Both context fixes and where public would be disallowed within do and both would benefit from a finer-grained definition visibility control.

@nomeata
Contributor
nomeata commented Jan 27, 2017

@int-index, in your proposal, foo seems to play a special role among the “exported” identifiers, for no good reason. That asymmetry strikes me as odd.

And it would also be nice to have a where where some definitions are public and some are not. But I don't want to think up a concrete syntax for that!

@goldfirere Note my proposed extension of context fixes param in … where … hinted at in the proposal, which would allow you to keep some definitions local to the context, and export others.

@ekmett
ekmett commented Jan 27, 2017

I've no real opinion on the extension or syntax yet, but from skimming:

To poke at @nomeata's variant on @jberryman's proposed syntax:

Wouldn't

context :: String -> OtherStuff -> ..

avoid having to declare a new (possibly conditional) lexeme? .. is already handled by the parser as an operator that is illegal in that position and ... is a fairly common operator choice that would be unfortunate to steal.

@hsenag
hsenag commented Jan 27, 2017

I think I agree with the view that this doesn't have a high enough power-to-weight ratio.

If we did have it, my straw man for the syntax would be a top-level lambda at declaration scope:

\(progName :: String) ->
    foo :: Maybe Int -> Either String Int
    foo Nothing  = Left $ progName ++ ": no number given

    bar :: Int -> Either String Int
    bar 0 = Left $ progName ++ ": zero no good"
    bar n = Right $ n + 1

Also, this proposal covers abstracting over value parameters. Is there a way it can be generalised to cover type parameters (i.e. introduced with scoped type variables + a forall), and constraints?

@ekmett
ekmett commented Jan 27, 2017

Assuming we go ahead with this:

Whatever syntax we wind up with it'd be nice if it ended with an existing lexeme that is known to introduce a layout block (e.g. where) since everyone understandably wants to use layout in the syntax and LambdaCase is already awkward enough for automatic indentation in editors.

@phadej
phadej commented Jan 27, 2017 edited

I kind of like the syntax of Agda's modules:

context Foo forall f a. (x :: f a) where
  some :: f [a]
  some = (:) <$> x <*> many

  many :: f [a]
  many = ...

  maybeUsed = ...

import Foo (some, many)

would read as

some :: forall f a. f a -> f [a]
some x = (:) <$> x <*> many x

/or?/ (and this is the question:)

some :: forall f a. f a -> f [a]
some x = (:) <$> x <*> many'
  where
    many' = ...
    some' = ...

In fact, if you take this "module in module" further, one could export directly from subcontexts:

module M (Foo.many) where

context Foo --- as above, or should we just reuse module?

and we don't necessarily need to import the context, we could use it qualified, or both!


TL;DR IMHO Agda's parameterised modules are neat. I don't see why there couldn't be local modules in Haskell. But I'm neutral on this proposal.

@parsonsmatt

I've been pondering this a lot, and I don't like it. The biggest reason is that the alternatives to the proposal appear to be superior to the proposal itself.

The new contexts are not a first class member of the language. Usage outside of the context provides no benefit of consistency: given the example context fixes progName in foo ..., there's nothing saying that I can't pass two different progNames to foo and bar when they're meant to be the same.

I think that first class modules a la Gabriel Gonzalez handles the "first class" concern well. The generator function alternative covers this case. Consider this:

{-# LANGUAGE RecordWildcards #-}
data Funcs = Funcs { foo :: Int -> String, bar :: String -> Int }

mkFuncs :: String -> Funcs
mkFuncs progName = Funcs{..}
  where
    foo i = ... progName ...
    bar s = ... progName ...

which gives us a few interesting uses that context doesn't:

-- in some other scope:
foo :: Int -> String
bar :: String -> Int
Funcs{..} = mkFuncs "foobar"

-- aliasing
Funcs{ foo = wat, bar = quux } = mkFuncs "watquux"

-- runtime determination
getLine >>= \lol -> print $ foo (mkFuncs lol) 3

For keeping parameters implicit and consistent, MonadReader works quite well. It has the advantage of having local :: MonadReader r m => (r -> r) -> m a -> m a, which allows you to modify the context if you need to. Since (->) r is an instance of MonadReader, they can also be called like normal functions if you don't have a monad in mind. Looking at the large real-world example, my brain wants to put that in (MonadReader (StepFun s, AnimationRate) m and some sort of MonadState. I'd like to see a comparison (and may make one myself).

I think the itch that we're trying to scratch is "a better module system." And I don't think that the added surface area of this proposal as-is is going to make that easier for people.

@nomeata
Contributor
nomeata commented Jan 27, 2017

The new contexts are not a first class member of the language. Usage outside of the context provides no benefit of consistency: given the example context fixes progName in foo ..., there's nothing saying that I can't pass two different progNames to foo and bar when they're meant to be the same.

I don’t follow. Sure, if you “enter the context” twice, you get to instantiate the parameter twice.

Or are you missing a way of instantiating the context once, but use two functions from that single single instantiation? That is indeed not provided.

The generator function alternative covers this case. Consider this: …

Yes, that works, but is a horribly convoluted way of writing this code, with lots of repetition. It also forces you do to write types (in the data type declaration), whereas context is compatible with type inference. Woudn’t it be nice to have some nice syntax for essentially that ;-)

Looking at the large real-world example, my brain wants to put that in (MonadReader (StepFun s, AnimationRate) m and some sort of MonadState. I'd like to see a comparison (and may make one myself).

But that would come at the expense of making everything monadic. I certainly prefer pure over monadic style whenever possible…

@nomeata
Contributor
nomeata commented Jan 27, 2017 edited

Also, this proposal covers abstracting over value parameters. Is there a way it can be generalised to cover type parameters (i.e. introduced with scoped type variables + a forall), and constraints?

That is a good question, I thought about it. It will work if we have good syntax for type signatures; here is how it looks with one of the type signature proposals:

context :: forall a. Show a => …
context in
  foo ::  a -> String
  foo = show

What also works, if you have a suitable value argument, is of course using PatternSignatures:

context (x :: Int -> a) in …
@ocharles
ocharles commented Feb 1, 2017

I was a little unsure about this proposal, but now that I'm doing some instrumenting of code, it does seem like it could really nicely tidy things up. I have a collection of counters, and a huge module of code that wants uniform access to the counters:

data Metrics = Metrics { latency :: Histogram, exceptions :: Counter, etc }

main :: IO ()
main = do
  metrics <- registerNewMetrics
  doStuff metrics

doStuff comes from another module with a whole heap of code in. I could imagine writing it as:

module Service (doStuff) where

context fixes Metrics{..} in
  doStuff :: IO ()
  helper :: IO ()
  helper2 :: IO ()

All three of those IO actions get access to a single shared metric store - I don't have to go threading metrics around, or shoving them into reader monads. It seems like a very elegant approach to easily adding some instrumentation.

@int-index

@ocharles Since in your case only doStuff is exported from Service, why wouldn't you use a where clause?

@ocharles
ocharles commented Feb 1, 2017 edited

@int-index I am then unable to develop this module in GHCI. I'm operating under the assumption that I could still access helper and helper2 in GHCI, provided I supply a Metrics object.

@int-index

I suppose the motivation section for this proposal could be extended with "GHCi access to definitions" because this hasn't occurred to me at all (I rarely use GHCi).

@nomeata
Contributor
nomeata commented Feb 1, 2017

I suppose the motivation section for this proposal could be extended with "GHCi access to definitions" because this hasn't occurred to me at all (I rarely use GHCi).

You would have the same with most of the alternatives (reader monad etc.), so I am unsure how to best express integrate this suggestion. But if you have wording for this in mind, feel free to edit it yourself!

@ocharles
ocharles commented Feb 1, 2017

You would have the same with most of the alternatives (reader monad etc.)

Though none with the convenience of just making a function call. With reader I have to faff around with running the reader monad, with implicits I have to bind them, and with reflection I have to reify. Non are as simply as just calling the function :)

@winterland1989
winterland1989 commented Feb 6, 2017 edited

With reader I have to faff around with running the reader monad

I don't follow why you think Reader is not an solution, isn't runReader simple enough?

@int-index
int-index commented Feb 6, 2017 edited

@winterland1989 Reader forces you to use do-notation, it also does not guarantee that the parameter will be the same (because there's local).

Both problems are solved by Given-style reflection, but it has problems of its own (you can call give twice, violating coherency).

And Reifies-style reflection requires you to thread the i parameter through the types. Hm-m. Will it be so bad with -XTypeApplications, though? We'd need -XTypePatterns as well (explicitly binding type variables in lambdas), to make reify look good instead of passing a Proxy. I start to think it might be a good alternative to the proposal.

Edit: Reifies-style reflection wouldn't work because we'd have to pass type indices the same way we pass normal function arguments now.

@winterland1989

Reader forces you to use do-notation,

You can use reader :: MonadReader m => (r -> a) -> m a to prefix your function to get a reader.

it also does not guarantee that the parameter will be the same (because there's local).

User can do whatever they want to an implicit binding too, at least a local in source code can sugges a different context is used here.

Overall i'm not convinced this's better than reader based solution since reader is such a simple, ubiquitous abstraction.

@siddhanathan
siddhanathan commented Feb 6, 2017 edited

Overall i'm not convinced this's better than reader based solution since reader is such a simple, ubiquitous abstraction.

Reader makes sense if you're trying to thread values between functions in a sequential manner, where subsequent sub-computations depend on the results of previous computations. If you don't want that behavior, then you probably don't want a monad. Reader is a monad, and you can modify the value being passed around using local, so it's way too powerful. Syntactic sugar for function parameters on the other hand doesn't give you this extra power.

@phadej
phadej commented Feb 6, 2017 edited

Sorry for a slight off-topic: out of curiosity, what's the original motivation for local, I don't see it used often. And e.g. monadLib defines ReaderM only with ask (as well as Writer's local and pass)?

@ocharles
ocharles commented Feb 6, 2017

You can use reader :: MonadReader m => (r -> a) -> m a to prefix your function to get a reader.

Not if the function I want to add a context to already uses MonadReader. Now I have to go find a variation of mtl that supports multiple environments. It's entirely possible that MonadReader might be used for something that crosses module boundaries (maybe a shared database connection pool).

@int-index

Now I have to go find a variation of mtl that supports multiple environments.

Shameless self-plug https://www.stackage.org/package/ether

And reader doesn't solve the issue anyway, because if you constantly call reader and runReader, it's no better than manually passing the parameter.

@winterland1989
winterland1989 commented Feb 6, 2017 edited

Now I have to go find a variation of mtl that supports multiple environments.

Shameless self-plug https://www.stackage.org/package/ether

Yes, there're many ways to extend reader, i personally use tuples and it works just fine.

I think there's no need to prove reader is a syntactic superior solution, since you can always add new syntactic construction to any language to provide a specific solution. The real problem is does reader solution is that bad so that we're force to add new syntactic construction? As we all concern that will complicate both language and language implementation.

As for haskell, a language we're comfortable with creating embeding DSL with monad(or any other functional construction) to solve these problem, i don't see the need to add a syntactic construction to get it done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment