Opened 4 years ago

Closed 20 months ago

Last modified 16 months ago

#10843 closed feature request (fixed)

Allow do blocks without dollar signs as arguments

Reported by: agibiansky Owned by: akio
Priority: normal Milestone: 8.6.1
Component: Compiler (Parser) Version: 7.10.2
Keywords: GHCProposal Cc: akio, mentheta
Operating System: Unknown/Multiple Architecture: Unknown/Multiple
Type of failure: None/Unknown Test Case:
Blocked By: Blocking:
Related Tickets: #11706 Differential Rev(s): Phab:D1219, Phab:D4260
Wiki Page:

Description (last modified by simonpj)

I would like the following to be valid Haskell code:

main = when True do
  putStrLn "Hello!"

Instead of requiring a dollar sign before the "do". This would parse as

main = when True (do
  putStrLn "Hello!")

Similarly, allow lambdas in the same way

main = forM values \value ->
  print value

parses as

main = forM values (\value ->
  print value)

One possible question: does this also do the same thing for LambdaCase? I think that since people expect lambda case to just be a simple desugaring it should also work, so then

main = forM values \case
    Just x -> print x
    Nothing -> print y

parses as

main = forM values (\case
    Just x -> print x
    Nothing -> print y)

Wiki page: ArgumentDo

Change History (48)

comment:1 Changed 4 years ago by agibiansky

I have an experimental implementation: https://phabricator.haskell.org/D1219

comment:2 Changed 4 years ago by agibiansky

Description: modified (diff)

comment:3 Changed 4 years ago by nomeata

Nice idea. Of course I am also annoyed by the $, but I sheepishly always assumed that there is a good reason for this.

Generally, I think this extension has comparable merits to similar syntax tweaks like nonincreasing indentation, so if the implementation is cleanly possible, I think this could go in.

Is there code that is valid with and without your extension, but with different semantics?

Last edited 4 years ago by nomeata (previous) (diff)

comment:4 Changed 4 years ago by agibiansky

Description: modified (diff)

comment:5 Changed 4 years ago by mpickering

How about case, if and let expressions?

I personally found it a bit inconsistent that id Record {..} parsed correctly so this does bring the other expression types in line with that behaviour. On the other hand, I think the $ is useful for the reader to indicate that what follows is the final single argument.

comment:6 Changed 4 years ago by agibiansky

I don't see a common usecase for case, if, and let, and think we should not include them. The motivation for this change is that do and lambdas are very commonly used in custom control structures (when, unless, for, forM, with*, and so on).

comment:7 Changed 4 years ago by Rufflewind

I don't see a common usecase for case, if, and let

I've certainly written code like this:

foo x = pure $ case x of
  Just _  -> True
  Nothing -> False

Personally I never understood why $ was required to begin with. There's no sensible way to interpret runST do … except as runST (do …).

comment:8 in reply to:  3 Changed 4 years ago by goldfire

Replying to nomeata:

Is there code that is valid with and without your extension, but with different semantics?

To me, this is the key question. I can't think of any. But let's dig deeper.

I'm looking at the Haskell 2010 Report for parsing these sorts of things.

Here's the relevant bit:

exp 	→ 	infixexp :: [context =>] type       (expression type signature)
	| 	infixexp
 
infixexp→ 	lexp qop infixexp                   (infix operator application)
	| 	- infixexp                          (prefix negation)
	| 	lexp
 
lexp 	→ 	\ apat1 … apatn -> exp              (lambda abstraction, n ≥ 1)
	| 	let decls in exp                    (let expression)
	| 	if exp [;] then exp [;] else exp    (conditional)
	| 	case exp of { alts }                (case expression)
	| 	do { stmts }                        (do expression)
	| 	fexp

fexp 	→ 	[fexp] aexp                         (function application)
 
aexp 	→ 	qvar                                (variable)
	| 	gcon                                (general constructor)
	| 	literal
	| 	( exp )                             (parenthesized expression)
	| 	( exp1 , … , expk )                 (tuple, k ≥ 2)
	| 	[ exp1 , … , expk ]                 (list, k ≥ 1)
	| 	[ exp1 [, exp2] .. [exp3] ]         (arithmetic sequence)
	| 	[ exp | qual1 , … , qualn ]         (list comprehension, n ≥ 1)
	| 	( infixexp qop )                    (left section)
	| 	( qop⟨-⟩ infixexp )                  (right section)
	| 	qcon { fbind1 , … , fbindn }        (labeled construction, n ≥ 0)
	| 	aexp⟨qcon⟩ { fbind1 , … , fbindn }   (labeled update, n ≥ 1)

From this grammar, I was inspired to try the following silliness:

{-# LANGUAGE Haskell2010 #-}

instance Num (IO ()) where
  negate = id

main = - do
  putStrLn "hi"

(Note the - after the main =.) This fails with a parse error. I'd love for someone else to check this against the grammar and see if I should report a separate bug. How does this work in the patch supplied?

In any case, even looking at the grammar, it looks like you've found a spot free in the grammar.

How many shift/reduce conflicts does the grammar have? At last check, GHC's parser had 47. This isn't great, but we don't want to increase this number!

Thanks for submitting a patch!

comment:9 Changed 4 years ago by agibiansky

@goldfire: The GHC parser (when I cloned master) had 48 shift/reduce conflicts, and this change introduces no new ones (there are still 48). (Actually I just went back to master and recompiled and checked, because I didn't check back when I cloned.) Anyway, this patch introduces no new shift/reduce conflicts.

The code

main = - do
  putStrLn "Hello"

causes a parse error with my patch as before, with or without -XArgumentDo enabled.

parse error on input do

comment:10 Changed 4 years ago by simonpj

Differential Rev(s): Phab:D1219

I'm not against this, but not wildly in favour either. It might be convenient, but it's another small wrinkle. Some thoughts

  • Haskell allows
       f MkT { x = 3 } y z
    
    meaning
      f (MkT {x = 3}) y z
    
    which I have always thought of as a bit of a mistake. The former looks (to my eye) too much like a function call with four argument.

However, you could argue that the do version is less harmful, because the initial do signals the start of a do block, whereas the initial MkT could be a bare constructor argument.

  • What do you intend for
      f x do { blarg } y z
    
    Is that equivalent to this?
      f x (do { blarg }) y z
    
    If so, better to say so.
  • Also give an example with layout. So perhaps
      f x do p <- ps
             return (p+1)
        z z
    
    is equivalent to
      f x (do { p <- ps; return (p+1) }) y z
    
    I think lambda-case is similar.
  • Lambda is different because it does not start implicit layout. So perhaps an un-parenthesised lambda can only appear as the last argument. Better say so.
  • If you allow lambda, why not if? And I suppose case?
      f x if this then that else the_other
    
    means
      f x (if this then that else the_other
    
    I suspect that this is a bridge too far, but I think it poses no more parsing difficulty than Lambda. Right?
  • What about infix functions? I.e. is this ok?
       f >>> do blarg
    
    Presumably the do binds more tightly than any operator?

I suggest that you make a wiki page on the GHC Trac to describe the extension, along with examples to cover these corner cases, and advertise it on the ghc-users list. That might elicit some info about whether people would find this useful or not.

comment:11 Changed 4 years ago by thomie

There was a giant (>75 email) discussion about this ticket here: https://mail.haskell.org/pipermail/haskell-cafe/2015-September/121217.html

Is there a summary/conclusion somewhere?

comment:12 Changed 4 years ago by bgamari

I tried to count the +1s and -1s but it seems to be fairly evenly split. Moreover, it seems like many users are fairly ambivalent on the matter.

Andrew's message from Sept 7 nicely reviews the various arguments for and against the proposal. I'll reproduce it and a few other notable points here,

Pro

  • It's easier to read than the alternative.
  • This extension removes syntactic noise.
  • This makes basic do-syntax more approachable to newbies; it is a commonly asked question as to why the $ is necessary.
  • This simplifies the resulting AST, potentially making it simpler for editors and other tools to do refactoring.
  • It's something that belongs in the main language, and if its something we'd consider for a mythical Haskell', it has to start as an extension.
  • It gets rid of some cases where using $ doesn't work because $ interacts with other infix operators being used in the same expression.
  • This would make do blocks consistent with record creation, where parentheses are skipped, allowing things such as return R { x = y}
  • This does not change the meaning of any old programs, only allows new ones that were previously forbidden.
  • This gets rid of the need for a specially-typed $ allowing runSt $ do ...
  • Richard Eisenberg points out that the proposal arguably makes the language more consistent, not less,

    I think that it makes the language *more* regular, not less: Look at https://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-220003, the Haskell 2010 Report on expression syntax. This proposal (if we include if, let, and case, along with \ and do, which would seem to make it more consistent) amounts to dropping the /lexp/ nonterminal and combining it with /aexp/.

Con

  • It's harder to read than the alternative.
  • Creating a language extension to get rid of a single character is overkill and unnecessary.
  • You can already get rid of the $ by just adding parentheses.
  • More and more syntactic "improvements" just fragment the language.
  • Although this is consistent with record syntax, record syntax without parents was a mistake originally.

comment:13 Changed 3 years ago by thomie

comment:14 Changed 3 years ago by akio

Cc: akio added

I would love this. Is there a way to move this forward? Write a wiki page?

Or is this proposal abandoned?

comment:15 Changed 3 years ago by ezyang

I suspect, in practice, it will involve finding someone with commit bits to champion the change, and then making sure all the loose bits and bobs from the original PR are tied up.

comment:16 Changed 3 years ago by Iceland_jack

Would this allow

foo = do
  liftIO do
    putStrLn do
      "hello" ++ "world"

meaning

foo = do
  liftIO $ do
    putStrLn $ do
      "hello" ++ "world"

comment:17 Changed 3 years ago by Iceland_jack

Old use case from A Concurrency Monad Based on Constructor Primitives, or, Being First-Class is not Enough:

addUpMain :: Process
addUpMain =
  OwnPid                        $ \self ->
  Fork (addUp self)             $ \server ->
  Send server (ListInt [1..20]) $ \() ->
  Receive                       $ \(Int n) -> 
  End

as

addUpMain :: Process
addUpMain =
  OwnPid                        \self       ->
  Fork (addUp self)             \server     ->
  Send server (ListInt [1..20]) \()         ->
  Receive                       \case Int n -> 
  End
addUpMain :: Process
addUpMain = OwnPid
  \self       -> Fork (addUp self)
  \server     -> Send server (ListInt [1..20]) 
  \()         -> Receive 
  \case Int n -> End

comment:18 Changed 3 years ago by Iceland_jack

EDSL Esqueleto

-- select $
-- from $ \p -> do
-- where_ (p ^. PersonName ==. val "John")
-- return p

select do
  from \p -> do
    where_ (p ^. PersonName ==. val "John")
    return p
-- select $
-- from $ \person -> do
-- where_ $ exists $
--          from $ \post -> do
--          where_ (post ^. BlogPostAuthorId ==. person ^. PersonId)
-- return person

select do
  from \person -> do
    where_ do
      exists do
        from \post -> 
          where_ (post ^. BlogPostAuthorId ==. person ^. PersonId)
    return person

HSpec

Gets rid of 6 $s in such a short example:

-- main :: IO ()
-- main = hspec $ do
--   describe "Prelude.head" $ do
--     it "returns the first element of a list" $ do
--       head [23 ..] `shouldBe` (23 :: Int)
-- 
--     it "returns the first element of an *arbitrary* list" $
--       property $ \x xs -> head (x:xs) == (x :: Int)
-- 
--     it "throws an exception if used with an empty list" $ do
--       evaluate (head []) `shouldThrow` anyException

main :: IO ()
main = hspec do
  describe "Prelude.head" do
    it "returns the first element of a list" do
      head [23 ..] `shouldBe` (23 :: Int)

    it "returns the first element of an *arbitrary* list" do
      property \x xs -> head (x:xs) == (x :: Int)

    it "throws an exception if used with an empty list" do
      evaluate (head []) `shouldThrow` anyException

concurrent

-- writePromise :: (MonadPar d i s m, Eq a) => Promise s a -> a -> m ()
-- writePromise (Promise m _) a = unsafeParIO $ do
--   a' <- evaluate a
--   mask_ $ do
--     t <- tryPutMVar m a'
--     unless t $ do
--       b <- readMVar m
--       unless (a' == b) $ throwIO Contradiction

writePromise :: (MonadPar d i s m, Eq a) => Promise s a -> a -> m ()
writePromise (Promise m _) a = unsafeParIO do
  a' <- evaluate a
  mask_ do
    t <- tryPutMVar m a'
    unless t do
      b <- readMVar m
      unless (a' == b) do throwIO Contradiction

Lucid

-- page2 = html_ $ do
--     head_ $ do
--       title_ "Introduction page."
--       with link_ [rel_ "stylesheet",type_ "text/css",href_ "screen.css"]
--     body_ $ do
--         with div_ [id_ "header"] "Syntax"
--         p_ "This is an example of Lucid syntax."
--         ul_ $ mapM_ (li_ . toHtml . show) [1,2,3]

page2 = html_ do
    head_ do
      title_ "Introduction page."
      with link_ [rel_ "stylesheet",type_ "text/css",href_ "screen.css"]
    body_ do
        with div_ [id_ "header"] "Syntax"
        p_ "This is an example of Lucid syntax."
        ul_ $ mapM_ (li_ . toHtml . show) [1,2,3]

Wrapped functions

newtype N = N (forall a. (a -> a) -> (a -> a))

-- s :: N -> N
-- s (N nat) = N (\suc zero -> suc (nat suc zero))
s :: N -> N
s (N nat) = N \suc zero -> suc (nat suc zero)

Actions as arguments

This reads great:

-- foo = forever $ do
--   this
--   that
foo = forever do
  this
  that

For loops

-- for_ "hello world" $ \ch -> do 
--   putChar ch
--   putChar ch
for_ "hello world" \ch -> do 
  putChar ch
  putChar ch

Getting close to the Pythonic ideal:

for x in xs:
  p
  q

Idris

syntax "for" {x} "in" [xs] ":" [body] = forLoop xs (\x => body)

main : IO ()
main = do for x in [1..10]:
              putStrLn ("Number " ++ show x)
          putStrLn "Done!"
Last edited 3 years ago by Iceland_jack (previous) (diff)

comment:19 Changed 3 years ago by nomeata

Generally sympathetic to this change, less parenthesis are good. For lambdas it looks a bit unusual, but I think I’d quickly adjust to parsing that.

Minor comment: In the wiki page you include {-# SCC #-} and {-# CORE #-}. I’d doubtful about them. I mentally parse them not as control structures, but as if they were normal functions (which happen to only work when used fully applied), and would be surprised if f a b {-# SCC #-} d e turn into f a b ({-# SCC #-} d e).

comment:20 Changed 3 years ago by simonpj

Description: modified (diff)

Akio has made a wiki page to specify the feature: ArgumentDo

comment:21 in reply to:  19 Changed 3 years ago by akio

Replying to nomeata:

Generally sympathetic to this change, less parenthesis are good. For lambdas it looks a bit unusual, but I think I’d quickly adjust to parsing that.

Minor comment: In the wiki page you include {-# SCC #-} and {-# CORE #-}. I’d doubtful about them. I mentally parse them not as control structures, but as if they were normal functions (which happen to only work when used fully applied), and would be surprised if f a b {-# SCC #-} d e turn into f a b ({-# SCC #-} d e).

Yes, I think that makes sense. Thank you for pointing it out.

I just moved SCC and CORE from the main proposal to the "Design Space" section.

comment:22 Changed 3 years ago by simonpj

I'm one of those who thinks that the fact that f R { x = e } means f (R { x = e }) is a mistake :-).

But I appreciate the wiki page, which makes the proposal much more concrete. If there is a reasonable level of support (which seems to be the case) I won't object.

I think it's be worth an email to ghc-users to draw attention to the wiki page and invite support or other feedback.

Simon

comment:23 Changed 3 years ago by rwbarton

I don't think parsing f a b {-# SCC #-} d e as f a b ({-# SCC #-} d e) is even an option, really, since these pragmas must be ignorable by compilers that don't understand them. Also, I think/hope this proposal does not change the parsing of any program that currently parses, right?

comment:24 Changed 3 years ago by Iceland_jack

I'm intrigued by multiple block arguments

f
  do x
  do y

Edit: Will it let you write

-- catch (readFile f)
--       (\e -> do let err = show (e :: IOException)
--                 hPutStr stderr ("Warning: Couldn't open " ++ f ++ ": " ++ err)
--                 return "")
catch 
  do readFile f
  \e -> do 
    let err = show (e :: IOException)
    hPutStr stderr ("Warning: Couldn't open " ++ f ++ ": " ++ err)
    return ""
-- bracket
--   (openFile "filename" ReadMode)
--   (hClose)
--   (\fileHandle -> do { ... })
bracket
  do openFile "filename" ReadMode
  do hClose
  \fileHandle -> do { ... }
Last edited 3 years ago by Iceland_jack (previous) (diff)

comment:25 in reply to:  23 Changed 3 years ago by akio

Replying to rwbarton:

I don't think parsing f a b {-# SCC #-} d e as f a b ({-# SCC #-} d e) is even an option, really, since these pragmas must be ignorable by compilers that don't understand them.

Agreed.

Also, I think/hope this proposal does not change the parsing of any program that currently parses, right?

I believe this is the case, although I don't have a proof. My rough argument is:

  1. This extension doesn't introduce any new conflict in the parser (shift/reduce or reduce/reduce)
  2. By chasing the new grammar you can see that do, lambda, etc. continue to parse as a lexp.

So if there is any change in the parsing of any currently-valid program, then it must be a result of somehow triggering an existing ambiguity in the grammar, but this seems unlikely.

comment:26 in reply to:  24 Changed 3 years ago by akio

Replying to Iceland_jack:

Edit: Will it let you write

Yes, your examples should be fine, and my preliminary implementation successfully parses them.

comment:27 in reply to:  22 ; Changed 3 years ago by goldfire

Replying to simonpj:

I'm one of those who thinks that the fact that f R { x = e } means f (R { x = e }) is a mistake :-).

I agree with you here, but I think the proposal in this ticket is still sensible, given that the perhaps-unexpected parsing started with a keyword. In the record update case, the perhaps-unexpected parsing isn't known until the open-brace, even though your brain has to parse the preceding space differently. To me, that's the real problem with the parsing of record-update: it's not left-to-right.

comment:28 in reply to:  27 Changed 3 years ago by maeder

Replying to goldfire:

Replying to simonpj:

I'm one of those who thinks that the fact that f R { x = e } means f (R { x = e }) is a mistake :-).

I agree with you here, but I think the proposal in this ticket is still sensible, given that the perhaps-unexpected parsing started with a keyword. In the record update case, the perhaps-unexpected parsing isn't known until the open-brace, even though your brain has to parse the preceding space differently. To me, that's the real problem with the parsing of record-update: it's not left-to-right.

I fully agree, too! The record syntax is unrelated to this proposal. Curly record braces are sort of a strong binding postfix operator and are thus different from plain parentheses.

@aiko I would omit the point "This would make do blocks consistent with record creation ..." under Pros on https://ghc.haskell.org/trac/ghc/wiki/ArgumentDo. Record creation is only another kind of a more "non-atomic" aexp in the grammar.

comment:29 Changed 3 years ago by maeder

@aiko I also agree fully with Richard Eisenberg and do not understand your objection.

Therefore I would like if you could add "it makes the language more regular" under Pros, omitted your last paragraph, and really simplified the proposed grammar rules accordingly (namely without lexp and openexp) on ArgumentDo.

(For a discussion/omission of group A and group B constructs I refer to https://mail.haskell.org/pipermail/glasgow-haskell-users/2016-July/026299.html)

comment:30 Changed 3 years ago by maeder

By saying "curly record braces are sort of a strongly binding postfix operator" I now realise that it is also possible to write i.e. "R {...} {...}" as nested record update although this is not really useful (since all fields can be merged into a single update).

The keywords, do, \, case, etc. in this proposal, however, are IMHO best characterized as weakly binding prefix operators (and I completely ignore curly layout braces here on purpose - I hardly use them).

Last edited 3 years ago by maeder (previous) (diff)

comment:31 Changed 3 years ago by maeder

@aiko The Cons point "You can already get rid of the $ by just adding parentheses" is only an argument against "$" (or the title of this issue) but not against ArgumentDo.

It is actually a Pros point: Allow to get rid of parentheses without using the $-workaround.

comment:32 Changed 3 years ago by Iceland_jack

Lambda motivation

-- phoas :: Phoas v (x -> y -> x)
-- phoas = PLam (\x -> PLam (\y -> PVar x))

phoas :: Phoas v (x -> y -> x)
phoas = PLam \x -> PLam \y -> PVar x
-- ex = λ (\x -> λ (\y -> x))

ex = λ \x -> λ \y -> x

Would this parse?

ex = λ\ x -> λ\ y -> x

comment:33 in reply to:  32 ; Changed 3 years ago by maeder

Replying to Iceland_jack:

[...]

-- ex = λ (\x -> λ (\y -> x))

ex = λ \x -> λ \y -> x }}}

Would this parse?

ex = λ\ x -> λ\ y -> x

Surely, this is a lexical issue. No space is needed between symbols (like "\") and (unicode) letters (like x, y, or λ), although I recommend to always leave a space, i.e. "f λ\ ..." would be parsed as "(f λ)\ ...".

comment:34 in reply to:  33 Changed 3 years ago by Iceland_jack

Replying to maeder:

Surely, this is a lexical issue. No space is needed between symbols (like "\") and (unicode) letters (like x, y, or λ), although I recommend to always leave a space, i.e. "f λ\ ..." would be parsed as "(f λ)\ ...".

I was wondering if we could mimic the upper-case lambda Λ by using a lower-case l for lambda

ex' = l\ x -> l\ y -> x

l\ look like a malformed lambda! (don't)

Last edited 3 years ago by Iceland_jack (previous) (diff)

comment:35 Changed 3 years ago by Iceland_jack

Moved to gist.

Last edited 2 years ago by Iceland_jack (previous) (diff)

comment:36 Changed 2 years ago by Iceland_jack

gist with examples

comment:37 Changed 2 years ago by mentheta

Cc: mentheta added

comment:38 Changed 2 years ago by nomeata

Is anyone still pursuing this?

I was just tossing the idea in my head of proposing multiple block arguments akin to

foo
  • one complex argument
  • second complex argument
    even on multiple lines
  • third complex argument

(with maybe not precisely this syntax), but if we had ArgumentDo I could smiply write, as was pointed out in The wiki proposal,

foo
  do one complex argument
  do second complex argument
       even on multiple lines
  do third complex argument

and I would have no need for a separate proposal…

comment:39 Changed 2 years ago by akio

Thank you for reminding me! I was waiting for the proposal process to start working and then forgot about this. I'll make a proposal shortly.

comment:40 Changed 23 months ago by mentheta

Akio, did you get around to submitting a proposal? I'd be interested in being able to use this extension. Also, could I be of help somehow?

comment:41 Changed 23 months ago by akio

Oh yes, oops. I have forgot about this. I have a half-written proposal. I'll finish it and submit it before the next week.

comment:43 Changed 23 months ago by mentheta

Thank you akio!

comment:44 Changed 21 months ago by akio

Owner: changed from agibiansky to akio

The proposal has been approved. I'll work on the implementation.

comment:45 Changed 21 months ago by akio

Differential Rev(s): Phab:D1219Phab:D1219, Phab:D4260
Status: newpatch

comment:46 Changed 20 months ago by Ben Gamari <ben@…>

In be84823/ghc:

Implement BlockArguments (#10843)

This patch implements the BlockArguments extension, as proposed at
https://github.com/ghc-proposals/ghc-proposals/pull/90. It also
fixes #10855 as a side-effect.

This patch adds a large number of shift-reduce conflicts to the parser.
All of them concern the ambiguity as to where constructs like `if` and
`let` end. Fortunately they are resolved correctly by preferring shift.

The patch is based on @gibiansky's ArgumentDo implementation (D1219).

Test Plan: ./validate

Reviewers: goldfire, bgamari, alanz, mpickering

Reviewed By: bgamari, mpickering

Subscribers: Wizek, dfeuer, gibiansky, rwbarton, thomie, mpickering, carter

GHC Trac Issues: #10843, #10855

Differential Revision: https://phabricator.haskell.org/D4260

comment:47 Changed 20 months ago by bgamari

Milestone: 8.6.1
Resolution: fixed
Status: patchclosed

Thanks Akio!

comment:48 Changed 16 months ago by RyanGlScott

Keywords: GHCProposal added
Note: See TracTickets for help on using tickets.