REPL ergonomics

I know the REPL is probably the least popular interface to Swift (as compared to Xcode projects, playground, iOS playground, swiftc/swift, and sharable online code "playgrounds"), but I think we should at least make some minimal effort to support a reasonable user experience.

We have a bit of a vi problem. Exiting the REPL is quite difficult. ctrl+d is a completely intuitive shortcut. Even if you knew you need to send EOF (as a newb not initiated with the backstory of terminal emulators, you don't), ctrl+d is completely unintuitive ("EOF".contains("d") == false) shortcut.

Consider these typical examples of what people might try to type to access help, or to quit.

Without Foundation imported

A typical novice might think "ctrl+c is how you quit terminal programs, right?" Nope.

  1> ^C
  1> ^C
  1> ^C

Silent failures, love it.

Help? Nope.

  1> help
error: repl.swift:1:1: error: use of unresolved identifier 'help'
help
^~~~



  1> --help
error: repl.swift:1:3: error: use of unresolved identifier 'help'
--help
  ^~~~



  1> \help
error: repl.swift:1:2: error: use of unresolved identifier 'help'
\help
 ^~~~

error: repl.swift:1:2: error: invalid component of Swift key path
\help
 ^

error: repl.swift:1:1: error: expression type 'WritableKeyPath<_, _>' is ambiguous without more context
\help
^~~~~
  1> ?
error: repl.swift:1:1: error: expected expression
?
^
  1> exit
error: repl.swift:1:1: error: use of unresolved identifier 'exit'
exit
^~~~
  1> quit
error: repl.swift:1:1: error: use of unresolved identifier 'quit'
quit
^~~~

With Foundation imported

This is where it gets interesting. The help and ? responses are the same, but exit is interesting:

exit on its own evaluates to an unapplied closure of type (Int32) -> Never. I'm sure everybody knows what 0x00000001000c6c80 $__lldb_expr11@nonobjc __C.exit(Swift.Int32) -> Swift.Never at repl10-fef73e..swift means, right? /s

  1> import Foundation
  2> exit
$R0: (Int32) -> Never = 0x00000001000c6c80 $__lldb_expr11`@nonobjc __C.exit(Swift.Int32) -> Swift.Never at repl10-fef73e..swift

Naively trying exit() obviously won't work, because you're missing the exist code as a Int32 argument. This is clear enough, but to a novice, it's not really obvious what this mystical number is, why it's required.

  1> import Foundation
  2> exit()
error: repl.swift:6:6: error: missing argument for parameter #1 in call
exit()
     ^
     <#Int32#>

Darwin.exit:1:13: note: 'exit' declared here
public func exit(_: Int32) -> Never

The results they yield make sense, from a strictly Swift perspective, in that they're not valid syntax, and the compiler is doing its best to handle the malformed Swift syntax. But I think it's abundantly clear that we should incorporate some special case syntax to that's discoverable and obvious, to support our users.

Compare how other languages do it:

Other languages set a much higher bar for user friendliness in this regard, and I think it would be a benefit to our community to look up to them as roll models, in this regard:

Ruby

Click to expand

quit and exit both exit right away. It's exactly what it says on the tin.

help is obvious, accessible, and interactive:

irb(main):001:0> help

Enter the method name you want to look up.
You can use tab to autocomplete.
Enter a blank line to exit.

>> # *awaits your input here*

I'll dock points for ctrl+c

irb(main):001:0> ^C
irb(main):001:0> 

Python

Click to expand

exit doesn't work directly, but at least it has a very clear message to help you:

>>> exit
Use exit() or Ctrl-D (i.e. EOF) to exit

quit is similar:

>>> quit
Use quit() or Ctrl-D (i.e. EOF) to exit

help is wonderful:

$ python3
Python 3.6.5 (default, Mar 30 2018, 06:42:10)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> help
Type help() for interactive help, or help(object) for help about object.
>>> help()

Welcome to Python 3.6's help utility!

If this is your first time using Python, you should definitely check out
the tutorial on the Internet at https://docs.python.org/3.6/tutorial/.

Enter the name of any module, keyword, or topic to get help on writing
Python programs and using Python modules.  To quit this help utility and
return to the interpreter, just type "quit".

To get a list of available modules, keywords, symbols, or topics, type
"modules", "keywords", "symbols", or "topics".  Each module also comes
with a one-line summary of what it does; to list the modules whose name
or summary contain a given string such as "spam", type "modules spam".

help> # *awaits your input here*

If you press enter, or type quit at this prompt, you get this very helpful message:

You are now leaving help and returning to the Python interpreter.
If you want to ask for help on a particular object directly from the
interpreter, you can type "help(object)".  Executing "help('string')"
has the same effect as typing a particular string at the help> prompt.

But on the down side, the handling of ctrl+c is even worse than Ruby and Swift:

>>> # *I type ctrl-c here*, nothing like is printed (e.g. like  "^C" in Ruby and Swift)
KeyboardInterrupt

Bash

Click to expand

exit is straight forward:

bash-3.2$ exit
exit

Unfortunately, quit doesn't work, even though an alias could have easily been made :

bash-3.2$ quit
bash: quit: command not found

help is a bit cryptic if you won't know the syntax they use for describing optional vs manditory options, but at least it's quit thorough:

bash-3.2$ help
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin16)
These shell commands are defined internally.  Type `help' to see this list.
Type `help name' to find out more about the function `name'.
Use `info bash' to find out more about the shell in general.
Use `man -k' or `info' to find out more about commands not in this list.

A star (*) next to a name means that the command is disabled.

 JOB_SPEC [&]                       (( expression ))
 . filename [arguments]             :
 [ arg... ]                         [[ expression ]]
 alias [-p] [name[=value] ... ]     bg [job_spec ...]
 bind [-lpvsPVS] [-m keymap] [-f fi break [n]
 builtin [shell-builtin [arg ...]]  caller [EXPR]
 case WORD in [PATTERN [| PATTERN]. cd [-L|-P] [dir]
 command [-pVv] command [arg ...]   compgen [-abcdefgjksuv] [-o option
 complete [-abcdefgjksuv] [-pr] [-o continue [n]
 declare [-afFirtx] [-p] [name[=val dirs [-clpv] [+N] [-N]
 disown [-h] [-ar] [jobspec ...]    echo [-neE] [arg ...]
 enable [-pnds] [-a] [-f filename]  eval [arg ...]
 exec [-cl] [-a name] file [redirec exit [n]
 export [-nf] [name[=value] ...] or false
 fc [-e ename] [-nlr] [first] [last fg [job_spec]
 for NAME [in WORDS ... ;] do COMMA for (( exp1; exp2; exp3 )); do COM
 function NAME { COMMANDS ; } or NA getopts optstring name [arg]
 hash [-lr] [-p pathname] [-dt] [na help [-s] [pattern ...]
 history [-c] [-d offset] [n] or hi if COMMANDS; then COMMANDS; [ elif
 jobs [-lnprs] [jobspec ...] or job kill [-s sigspec | -n signum | -si
 let arg [arg ...]                  local name[=value] ...
 logout                             popd [+N | -N] [-n]
 printf [-v var] format [arguments] pushd [dir | +N | -N] [-n]
 pwd [-LP]                          read [-ers] [-u fd] [-t timeout] [
 readonly [-af] [name[=value] ...]  return [n]
 select NAME [in WORDS ... ;] do CO set [--abefhkmnptuvxBCHP] [-o opti
 shift [n]                          shopt [-pqsu] [-o long-option] opt
 source filename [arguments]        suspend [-f]
 test [expr]                        time [-p] PIPELINE
 times                              trap [-lp] [arg signal_spec ...]
 true                               type [-afptP] name [name ...]
 typeset [-afFirtx] [-p] name[=valu ulimit [-SHacdfilmnpqstuvx] [limit
 umask [-p] [-S] [mode]             unalias [-a] name [name ...]
 unset [-f] [-v] [name ...]         until COMMANDS; do COMMANDS; done
 variables - Some variable names an wait [n]
 while COMMANDS; do COMMANDS; done  { COMMANDS ; }

ctrl+c just makes fresh new lines, which is acceptable given that bash is a shell, not just a REPL, so the expectations are a bit different

bash-3.2$
bash-3.2$

ctrl+d displays the text exit and then exits.

bash-3.2$ exit
6 Likes

Well, Ctrl+C is, like you noticed in your examples, not typically how you quit interactive cli things, including pretty much every shell. Ctrl+D works to quit in pretty much every repl-thing as well.

You left out this bit, the first thing the repl outputs:

Welcome to Apple Swift version 4.1.2 (swiftlang-902.0.54 clang-902.0.39.2). Type :help for assistance.

Using :help you will discover :quit (or :q), which works as advertised.

That being said, the output of :help is quite large, and is (probably confusingly so for novices) the LLDB help with a little prefix (that you may need to scroll up a bit to see on some monitors). Reducing this to basic info and commands, plus a clear way to get more info would probably be good.

9 Likes

Oh I did. I didn't even notice it, because it comes after a bit of random spam I've grown desensitized to:

$ swift
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/usr/local/Cellar/python@2/2.7.15/Frameworks/Python.framework/Versions/2.7/lib/python2.7/copy.py", line 52, in <module>
    import weakref
  File "/usr/local/Cellar/python@2/2.7.15/Frameworks/Python.framework/Versions/2.7/lib/python2.7/weakref.py", line 14, in <module>
    from _weakref import (
ImportError: cannot import name _remove_dead_weakref
Welcome to Apple Swift version 4.0.3 (swiftlang-900.0.74.1 clang-900.0.39.2). Type :help for assistance.
  1>

Another problem here, is that the description for :q and :exit is Quit the LLDB debugger. A Swift novice wouldn't know the the Swift REPL is implemented over the LLDB debugger (nor should they have to), so it's not immediately obvious that this is what they need.

1 Like

I've noticed this python issue as well. It seems to be caused by you using any version other than the system version of python with lldb or swift. You can work around this by aliasing swift:

alias swift='PATH="/usr/bin" swift'
2 Likes

Have you done any preliminary investigation into the root cause?

There's some more context on this here: python2 breaking lldb on osx · Issue #2730 · Homebrew/homebrew-core · GitHub

From a little more investigation I believe this is because lldb loads the system level python binary, but the files in your $PATH trump those in the system, so you end up trying to import a symbol which exists in the version from homebrew, but not in the one from the system. You can see a bit of this if you run lldb like this:

DYLD_PRINT_LIBRARIES=1 lldb 2>&1 | tee foo.log

In the log you'll see it load system python:

dyld: loaded: /System/Library/Frameworks/Python.framework/Versions/2.7/Python

And then some dylibs from the brewed version:

dyld: loaded: /usr/local/opt/python@2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/_locale.so
dyld: loaded: /usr/local/opt/python@2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/_ctypes.so
dyld: unloaded: /usr/local/opt/python@2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/_ctypes.so
dyld: loaded: /usr/local/opt/python@2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/_functools.so
dyld: loaded: /usr/local/opt/python@2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/itertools.so
dyld: loaded: /usr/local/opt/python@2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/operator.so
dyld: loaded: /usr/local/opt/python@2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/readline.so

I'm assuming the version inconsistencies cause some of this (in my case 2.7.10 from the system and 2.7.14 from homebrew).

You can see that this symbol exists in brewed python:

% rg -al remove_dead_weakref `brew --prefix python@2`
/usr/local/opt/python@2/Frameworks/Python.framework/Versions/2.7/Python
/usr/local/opt/python@2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/weakref.py

But not in the system python:

rg -al remove_dead_weakref /System/Library/Frameworks/Python.framework
2 Likes

Please see this thread: Swift REPL starts with error when homebrew python is installed

About the topic in question, I also find it bizarre that you complain about requiring ctrl-d instead of ctrl-c to exit, when (as far as I know) every single interactive cli thing exits with ctrl-d. Changing this would be creating the "vi problem" that you mention, not the other way around.

2 Likes

I'm not suggesting it be changed. We can have aliased functionality, as with how quit and exit are both accepted in both python and ruby.

New programmers often have their first experience writing simple terminal based apps (in C, Java, etc.). These quit exclusively with ctrl-d (the programs don't necessarily take input, and any response to EOF would have to be programmed in).

For the record, I work a lot with terminals and REPLs (in different languages), and I didn't actually know that "ctrl-d" is "how you generally quit things". Most REPLs that I know have something like "exit" that will work. That said, I'm a vi user, so ":q" is also "natural" to me.

I'll agree though, that in general, the REPL is far from ergonomic, especially compared to other REPLs - like Python and Ruby, as mentioned. Even worse is that the REPL is not integrated with the SPM. This makes the REPL unsuitable for the kind of "play around with a library" discovery process that languages like Node or Ruby excel at.

2 Likes

If we moved the "Type :help for assistance." sentence to its own line, it might be easier for people to notice. Maybe change it to "Type :help for assistance; ctrl+D to quit". That might be an easy, useful first step.

5 Likes

at least you can use the REPL

$ swift
Welcome to Swift version 4.2-dev (LLVM 70f121e1f0, Clang 4c555650a6, Swift bb9532c588). Type :help for assistance.
  1> 2 + 2
error: Couldn't lookup symbols:
  swift_beginAccess
  swift_endAccess

  1>  
3 Likes

I would love to add a couple of features to the REPL at some point - even just the ability to reset the current instance would be amazing.

3 Likes

Are you on Linux? There is a bug that you need to import Foundation first, compare Ubuntu developer snapshot requires Foundation for print statement and [SR-7801] Ubuntu developer snapshot requires Foundation for standard library symbols · Issue #4368 · apple/llvm-project · GitHub.

$ usr/bin/swift
Welcome to Swift version 4.2-dev (LLVM ec4d8ae334, Clang 5a454fa3d6, Swift 0ca361f487). Type :help for assistance.
  1> 2 + 2
error: Couldn't lookup symbols:
  swift_beginAccess
  swift_endAccess

  1> ^D

$ usr/bin/swift
Welcome to Swift version 4.2-dev (LLVM ec4d8ae334, Clang 5a454fa3d6, Swift 0ca361f487). Type :help for assistance.
  1> import Foundation
  2> 2 + 2
$R0: Int = 4
  3>
2 Likes

There's no harm done in being accommodating to new users (or those who only use the REPL occasionally). help, ?, ^C and quit should at least give helpful messages. Just like Python or Ruby do.

2 Likes

I like that, but would change it to

Type :help for assistance; :quit to quit

There is also Swift on Windows, where EOF is signaled by "F6" instead of "Ctrl-D".

3 Likes

I never had a problem with leaving the REPL, but I may be biased as a long-term Unix and occasional vi user. The top Google hits for "Swift REPL" are

and neither of them mentions "quit" or "exit", so for a command-line novice it may indeed be difficult to find that information. Austin's suggestion to make the "Type :help ..." hint more prominent looks like a good compromise to me.

One can also enter the REPL from within a lldb debugger session. Here some hint that a single colon returns to lldb would really be useful:

(lldb) repl
1> 1+2
$R0: Int = 3
2> :
(lldb)
2 Likes

Ding ding ding. Initiating commands with : is not intuitive. Clearly, just look at Vi's infamy for being an unescapable trap for novices.

1 Like

I wonder if we could add some repl-specific implicit declarations when running in REPL mode, so that exit and/or quit work, or at least give you a message telling you how to exit. For instance, there could be a declaration:

let exit = "Press ^D to exit"

so if you type exit, you at least get a helpful message. Python does this, for instance.

16 Likes

hm. that fixed it.

If we agree that this is a problem, and that something needs to be done, I wouldn't settle for just some implicitly declared string? For one, ^ is not obvious to represent the control key. We can do better, but first we need to agree that this is a problem, and it looks to me like that has been accepted.