New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add “context fixes” to the language. #40

Closed
wants to merge 12 commits into
base: master
from

Conversation

Projects
None yet
@nomeata
Contributor

nomeata commented Jan 20, 2017

Rendered

@parsonsmatt

This comment has been minimized.

Show comment
Hide comment
@parsonsmatt

parsonsmatt Jan 20, 2017

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?

parsonsmatt commented Jan 20, 2017

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?

Show outdated Hide outdated 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.

This comment has been minimized.

@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"?

@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

This comment has been minimized.

Show comment
Hide comment
@jberryman

jberryman Jan 20, 2017

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 commented Jan 20, 2017

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

This comment has been minimized.

Show comment
Hide comment
@jberryman

jberryman Jan 20, 2017

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 commented Jan 20, 2017

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

This comment has been minimized.

Show comment
Hide comment
@jberryman

jberryman Jan 20, 2017

@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

jberryman commented Jan 20, 2017

@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

Incorporate suggestions by @markus2330
in particular, section on related work.
@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 20, 2017

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@jberryman

jberryman Jan 20, 2017

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

jberryman commented Jan 20, 2017

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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 20, 2017

Contributor
context new where
  foo = X <$> reused <*> ...
  reused = ... bottom ...
  bar = ... bottom ...
  baz = ... bottom ..
  bottom = doStuffWith new

should do exactly what you want.

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

This comment has been minimized.

Show comment
Hide comment
@hsenag

hsenag Jan 20, 2017

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.

hsenag commented Jan 20, 2017

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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 20, 2017

Contributor

“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.

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.

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Jan 21, 2017

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?

ghost commented Jan 21, 2017

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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 21, 2017

Contributor

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

Contributor

nomeata commented Jan 21, 2017

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

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Jan 21, 2017

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.

ghost commented Jan 21, 2017

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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 21, 2017

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@simonpj

simonpj Jan 23, 2017

Contributor

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.

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@simonmar

simonmar Jan 23, 2017

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.

simonmar commented Jan 23, 2017

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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 23, 2017

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 23, 2017

Contributor

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.

Contributor

nomeata commented Jan 23, 2017

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

This comment has been minimized.

Show comment
Hide comment
@simonmar

simonmar Jan 23, 2017

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.

simonmar commented Jan 23, 2017

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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 23, 2017

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@Icelandjack

Icelandjack Jan 23, 2017

Contributor

Are other constructs allowed in contexts?

Type class declarations + instances, ..

Contributor

Icelandjack commented Jan 23, 2017

Are other constructs allowed in contexts?

Type class declarations + instances, ..

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 23, 2017

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@simonmar

simonmar Jan 23, 2017

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.

simonmar commented Jan 23, 2017

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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 23, 2017

Contributor

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.

Contributor

nomeata commented Jan 23, 2017

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

This comment has been minimized.

Show comment
Hide comment
@ocharles

ocharles Jan 23, 2017

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.

ocharles commented Jan 23, 2017

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

This comment has been minimized.

Show comment
Hide comment
@mchakravarty

mchakravarty Jan 24, 2017

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.

mchakravarty commented Jan 24, 2017

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

This comment has been minimized.

Show comment
Hide comment
@Roxxik

Roxxik 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

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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 24, 2017

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@rrnewton

rrnewton Jan 24, 2017

@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.

rrnewton commented Jan 24, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 24, 2017

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@int-index

int-index Jan 27, 2017

Contributor

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
    ...
Contributor

int-index commented Jan 27, 2017

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

This comment has been minimized.

Show comment
Hide comment
@goldfirere

goldfirere Jan 27, 2017

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.

Contributor

goldfirere commented Jan 27, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@int-index

int-index Jan 27, 2017

Contributor

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.

Contributor

int-index commented Jan 27, 2017

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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 27, 2017

Contributor

@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.

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

This comment has been minimized.

Show comment
Hide comment
@ekmett

ekmett 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.

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

This comment has been minimized.

Show comment
Hide comment
@hsenag

hsenag 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?

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

This comment has been minimized.

Show comment
Hide comment
@ekmett

ekmett 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.

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

This comment has been minimized.

Show comment
Hide comment
@phadej

phadej Jan 27, 2017

Contributor

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.

Contributor

phadej commented Jan 27, 2017

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

This comment has been minimized.

Show comment
Hide comment
@parsonsmatt

parsonsmatt Jan 27, 2017

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.

parsonsmatt commented Jan 27, 2017

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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 27, 2017

Contributor

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…

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

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Jan 27, 2017

Contributor

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 …
Contributor

nomeata commented Jan 27, 2017

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

This comment has been minimized.

Show comment
Hide comment
@ocharles

ocharles 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.

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

This comment has been minimized.

Show comment
Hide comment
@int-index

int-index Feb 1, 2017

Contributor

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

Contributor

int-index commented Feb 1, 2017

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

@ocharles

This comment has been minimized.

Show comment
Hide comment
@ocharles

ocharles Feb 1, 2017

@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.

ocharles commented Feb 1, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@int-index

int-index Feb 1, 2017

Contributor

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).

Contributor

int-index 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).

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Feb 1, 2017

Contributor

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!

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

This comment has been minimized.

Show comment
Hide comment
@ocharles

ocharles 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 :)

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

This comment has been minimized.

Show comment
Hide comment
@winterland1989

winterland1989 Feb 6, 2017

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?

winterland1989 commented Feb 6, 2017

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

This comment has been minimized.

Show comment
Hide comment
@int-index

int-index Feb 6, 2017

Contributor

@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.

Contributor

int-index commented Feb 6, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@winterland1989

winterland1989 Feb 6, 2017

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.

winterland1989 commented Feb 6, 2017

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.

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Feb 6, 2017

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.

ghost commented Feb 6, 2017

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

This comment has been minimized.

Show comment
Hide comment
@phadej

phadej Feb 6, 2017

Contributor

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)?

Contributor

phadej commented Feb 6, 2017

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

This comment has been minimized.

Show comment
Hide comment
@ocharles

ocharles 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).

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

This comment has been minimized.

Show comment
Hide comment
@int-index

int-index Feb 6, 2017

Contributor

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.

Contributor

int-index commented Feb 6, 2017

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

This comment has been minimized.

Show comment
Hide comment
@winterland1989

winterland1989 Feb 6, 2017

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.

winterland1989 commented Feb 6, 2017

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.

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Mar 23, 2017

Contributor

I just found this nice quote on https://ghc.haskell.org/trac/ghc/wiki/ViewPatternsAlternative

However, sometimes modest syntactic sugar can have profound consequences. In this case, it's possible that people would start routinely hiding the data representation and exporting view functions instead, which would be an excellent thing.

and I’d like to steal this and state:

However, sometimes modest syntactic sugar can have profound consequences. In this case, it's possible that people would start routinely being explicit in which parameters are constant across a set of functions, and hide the distraction of passing them around explicitly.

Contributor

nomeata commented Mar 23, 2017

I just found this nice quote on https://ghc.haskell.org/trac/ghc/wiki/ViewPatternsAlternative

However, sometimes modest syntactic sugar can have profound consequences. In this case, it's possible that people would start routinely hiding the data representation and exporting view functions instead, which would be an excellent thing.

and I’d like to steal this and state:

However, sometimes modest syntactic sugar can have profound consequences. In this case, it's possible that people would start routinely being explicit in which parameters are constant across a set of functions, and hide the distraction of passing them around explicitly.

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Apr 14, 2017

Contributor

TODO for myself: Make this proposal modular (or break it up in multiple proposals), and add a new aspect of scoped language extensions, for language extensions that can easily applied to a part of a module (I am thinking of RebindableSyntax here, and maybe others).

Contributor

nomeata commented Apr 14, 2017

TODO for myself: Make this proposal modular (or break it up in multiple proposals), and add a new aspect of scoped language extensions, for language extensions that can easily applied to a part of a module (I am thinking of RebindableSyntax here, and maybe others).

@wrengr

This comment has been minimized.

Show comment
Hide comment
@wrengr

wrengr Sep 10, 2017

I appreciate the concern, and would love to find a solution to it; but having used sections like these in Coq, I am firmly convinced they are not a good design. It is all too common for folks to float everything they possibly can out, which ends up rendering the type signatures inscrutable and replaces the "mutation" with "global variables". I'm sure there's a better design to be found

wrengr commented Sep 10, 2017

I appreciate the concern, and would love to find a solution to it; but having used sections like these in Coq, I am firmly convinced they are not a good design. It is all too common for folks to float everything they possibly can out, which ends up rendering the type signatures inscrutable and replaces the "mutation" with "global variables". I'm sure there's a better design to be found

@nomeata

This comment has been minimized.

Show comment
Hide comment
@nomeata

nomeata Feb 23, 2018

Contributor

I guess I am not following up with this; closing.

Contributor

nomeata commented Feb 23, 2018

I guess I am not following up with this; closing.

@nomeata nomeata closed this Feb 23, 2018

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