[#100689] [Ruby master Feature#17303] Make webrick to bundled gems or remove from stdlib — hsbt@...
Issue #17303 has been reported by hsbt (Hiroshi SHIBATA).
11 messages
2020/11/02
[#100852] [Ruby master Feature#17326] Add Kernel#must! to the standard library — zimmerman.jake@...
SXNzdWUgIzE3MzI2IGhhcyBiZWVuIHJlcG9ydGVkIGJ5IGpleiAoSmFrZSBaaW1tZXJtYW4pLg0K
24 messages
2020/11/14
[#100930] [Ruby master Feature#17333] Enumerable#many? — masafumi.o1988@...
Issue #17333 has been reported by okuramasafumi (Masafumi OKURA).
10 messages
2020/11/18
[#101071] [Ruby master Feature#17342] Hash#fetch_set — hunter_spawn@...
Issue #17342 has been reported by MaxLap (Maxime Lapointe).
26 messages
2020/11/25
[ruby-core:100853] Re: [Ruby master Feature#17326] Add Kernel#must! to the standard library
From:
Austin Ziegler <halostatue@...>
Date:
2020-11-15 05:19:44 UTC
List:
ruby-core #100853
```ruby
task.
must!.
mailing_params.
must!.
fetch('template_context')
```
I think that this is unreadable and I would never use it in any of my
code=E2=80=94and would reject any PR that contains it in any project I main=
tain. It
reduces the readability of the code to the detriment of debugging bad
runtime behaviour, as it exchanges a `NoMethodError` for a `TypeError`.
Within the small range of methods that `nil` responds to, the type checkers
should be able to analyze that
```ruby
task.mailing_params.fetch('template_context')
```
implies that both `task` and the return from `mailing_params` must not be
`nil` because there is no safe navigation specified.
If something must be done (which to my mind is questionable), it should
probably be done with syntax, because only then can you get something
meaningfully better at run time than either NoMethodError mailing_params on
nil or TypeError nil must! (which is arguably worse at runtime).
I admit that I=E2=80=99m biased against the type checkers as they exist. I=
=E2=80=99ve tried
using both Sorbet and RBS with a gem that I maintain, and I found both of
them completely unusable for this 16 year old gem. I got further with
Sorbet, but an issue that I opened=E2=80=A1 was closed with unhelpful sugge=
stions
(`T.unsafe` is not an answer; I would need that in about 90% of the code,
at which point Sorbet becomes a complete waste of time because the
resulting code is completely unreadable).
I=E2=80=99m not at all willing to give up readability to help programs of
questionable utility do their jobs. IMNSHO, type annotation should be fully
*externally* opt-in for Ruby programs, with all of the readability warts
that implies for those people who choose to use type checkers like Sorbet.
#must! is a type annotation wart that I don=E2=80=99t think belongs in the =
standard
library.
=E2=80=A1 https://github.com/sorbet/sorbet/issues/3252
-a
On Sat, Nov 14, 2020 at 6:15 PM <zimmerman.jake@gmail.com> wrote:
> Issue #17326 has been reported by jez (Jake Zimmerman).
>
> ----------------------------------------
> Feature #17326: Add Kernel#must! to the standard library
> https://bugs.ruby-lang.org/issues/17326
>
> * Author: jez (Jake Zimmerman)
> * Status: Open
> * Priority: Normal
> ----------------------------------------
> # Abstract
>
> We should add a method `Kernel#must!` (name TBD) which raises if `self` i=
s
> `nil` and returns `self` otherwise.
>
>
> # Background
>
> Ruby 3 introduces type annotations for the standard library.
> Type checkers consume these annotations, and report errors for type
> mismatches.
> One of the most common and most valuable type errors is whether `nil` is
> allowed as an argument or return value.
> Sorbet's type system tracks this, and RBS files have syntax for annotatin=
g
> whether `nil` is allowed or not.
>
> Since Sorbet checks proper usage of `nil`, it requires code that looks
> like this:
>
> ```ruby
> if thing.nil?
> raise "The thing was nil"
> end
>
> thing.do_something
> ```
>
> This is good because it forces the programmer to acknowledge that the
> thing might be `nil`, and declare
> that they'd rather raise an exception in that case than handle the `nil`
> (of course, there are many other
> times where `nil` is both possible and valid, which is why Sorbet forces
> at least considering in all cases).
>
> It is annoying and repetitive to have to write these `if .nil?` checks
> everywhere to ignore the type error,
> so Sorbet provides it as a library function, called `T.must`:
>
> ```ruby
> T.must(thing).do_something
> ```
>
> Sorbet knows that the call to `T.must` raises if `thing` is `nil`.
> To make this very concrete, here's a Sorbet playground where you can see
> this in action:
>
> [=E2=86=92 View on sorbet.run](
> https://sorbet.run/#%23%20typed%3A%20true%0Aextend%20T%3A%3ASig%0A%0Aclas=
s%20Thing%0A%20%20def%20do_something%3B%20end%0Aend%0A%0Asig%20%7Bparams(th=
ing%3A%20T.nilable(Thing)).void%7D%0Adef%20example1(thing)%0A%20%20%23%20er=
ror%2C%20might%20be%20nil%3A%0A%20%20thing.do_something%0Aend%0A%0Asig%20%7=
Bparams(thing%3A%20T.nilable(Thing)).void%7D%0Adef%20example2(thing)%0A%20%=
20if%20thing.nil%3F%0A%20%20%20%20raise%20%22The%20thing%20was%20nil%22%0A%=
20%20end%0A%0A%20%20%23%20no%20error%2C%20because%20it's%20after%20the%20%6=
0if%20.nil%3F%60%20check%3A%0A%20%20thing.do_something%0Aend%0A%0Asig%20%7B=
params(thing%3A%20T.nilable(Thing)).void%7D%0Adef%20example3(thing)%0A%20%2=
0%23%20no%20error%2C%20because%20it's%20after%20the%20%60if%20.nil%3F%60%20=
check%3A%0A%20%20T.must(thing).do_something%0Aend
> )
>
> You can read more about `T.must` in the [Sorbet documentation](
> https://sorbet.org/docs/type-assertions#tmust).
>
>
> # Problem
>
> While `T.must` works, it is not ideal for a couple reasons:
>
> 1. It is not a method, so you can't `map` it over a list using
> `Symbol#to_proc`. Instead, you have to expand the block:
>
> ```ruby
> array_of_integers =3D array_of_nilable_integers.map {|x| T.must(x) }
> ```
>
> 2. It leads to a weird outward spiral of flow control, which disrupts
> method chains:
>
> ```ruby
> # =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=90
> # =E2=94=82 =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=90 =E2=94=82
> # =E2=96=BC =E2=96=BC =E2=94=82 =E2=94=82
> T.must(T.must(task).mailing_params).fetch('template_context')
> # =E2=94=82 =E2=94=82 =E2=96=B2 =E2=96=B2
> # =E2=94=82 =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=98 =E2=
=94=82
> # =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=80=E2=94=80=E2=94=98
> ```
>
> compare that control flow with this:
>
> ```ruby
> # =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=90=E2=94=8C=E2=
=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=90=E2=94=8C=E2=94=80=E2=94=80=E2=94=
=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=
=E2=94=80=E2=94=80=E2=94=90=E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=
=94=90
> # =E2=94=82 =E2=96=BC=E2=94=82 =E2=96=BC=E2=94=82 =
=E2=96=BC=E2=94=82 =E2=96=BC
> task.must!.mailing_params.must!.fetch('template_context')
> ```
>
> 3. It is in a Sorbet-specific gem. We do not intend for Sorbet to be the
> only type checker.
> It would be nice to have such a method in the Ruby standard library s=
o
> that it can be shared by all type checkers.
>
> 4. This method can make Ruby codebases that **don't** use type checkers
> more robust!
> `Kernel#must!` could be an easy way to assert invariants early.
> Failing early makes it more likely that a test will fail, rather than
> getting `TypeError`'s and `NoMethodError`'s in production.
> This makes all Ruby code better, not just the Ruby code using types.
>
>
> # Proposal
>
> We should extend the Ruby standard library with something like this::
>
> ```ruby
> module Kernel
> def must!; self; end
> end
>
> class NilClass
> def must!
> raise TypeError.new("nil.must!")
> end
> end
> ```
>
> These methods would get type annotations that look like this:
> (using Sorbet's RBI syntax, because I don't know RBS well yet)
>
> ```ruby
> module Kernel
> sig {returns(T.self_type)}
> def must!; end
> end
>
> class NilClass
> sig {returns(T.noreturn)}
> def must!; end
> end
> ```
>
> What these annotations say:
>
> - In `Kernel#must!`, the return value is `T.self_type`, or "whatever the
> type of the receiver was."
> That means that `0.must!` will have type `Integer`, `"".must!` will hav=
e
> type `String`, etc.
>
> - In `NilClass#must!`, there is an override of `Kernel#must!` with return
> type `T.noreturn`.
> This is a fancy type that says "this code either infinitely loops or
> raises an exception."
> This is the name for Sorbet's [bottom type](
> https://en.wikipedia.org/wiki/Bottom_type), if you
> are familiar with that terminology.
>
> Here is a Sorbet example where you can see how these annotations behave:
>
> [=E2=86=92 View on sorbet.run](
> https://sorbet.run/#%23%20typed%3A%20true%0A%0Amodule%20Kernel%0A%20%20T%=
3A%3ASig%3A%3AWithoutRuntime.sig%20%7Breturns(T.self_type)%7D%0A%20%20def%2=
0must!%3B%20self%3B%20end%0Aend%0A%0Aclass%20NilClass%0A%20%20T%3A%3ASig%3A=
%3AWithoutRuntime.sig%20%7Breturns(T.noreturn)%7D%0A%20%20def%20must!%0A%20=
%20%20%20raise%20TypeError.new(%22nil.must!%22)%0A%20%20end%0Aend%0A%0Axs%2=
0%3D%20T%3A%3AArray%5BInteger%5D.new(%5B0%5D)%0AT.reveal_type(xs.first)%20%=
20%20%20%20%20%20%23%20T.nilable(Integer)%0AT.reveal_type(xs.first.must!)%2=
0%23%20Integer%0A%0Ays%20%3D%20T%3A%3AArray%5BT.nilable(Integer)%5D.new(%5B=
0%2C%20nil%2C%201%2C%20nil%2C%202%5D)%0AT.reveal_type(ys)%20%20%20%20%20%20=
%20%20%20%20%20%20%20%20%23%20T%3A%3AArray%5BT.nilable(Integer)%5D%0AT.reve=
al_type(ys.map(%26%3Amust!))%20%23%20T%3A%3AArray%5BInteger%5D
> )
>
> # Alternatives considered
>
> There was some discussion of this feature at the Feb 2020 Ruby Types
> discussion:
>
> Summarizing:
>
> - Sorbet team frequently recommends people to use `xs.fetch(0)` instead o=
f
> `T.must(xs[0])`
> on `Array`'s and `Hash`'s because it chains and reads better.
> `.fetch` not available on other classes.
>
> - It's intentional that `T.must` requires as many characters as it does.
> Making it slightly annoying to type encourages developers to refactor
> their code so that `nil` never occurs.
>
> - There was a proposal to introduce new syntax like `thing.!!`. This is
> currently a syntax error.
>
> **Rebuttal**: There is burden to introducing new syntax. Tools like
> Rubocop, Sorbet, and syntax highlighting
> plugins have to be updated. Also: it is hard to search for on Google (a=
s
> a new Ruby developer). Also: it
> is very short=E2=80=94having something slightly shorter makes people th=
ink about
> whether they want to type it out
> instead of changing the code so that `nil` can't occur.
>
> Another alternative would be to dismiss this as "not useful / common
> enough". I don't think that's true.
> Here are some statistics from Stripe's Ruby monolith (~10 million lines o=
f
> code):
>
> | methood | percentage of files mentioning method | number of occurrences
> of method |
> | --- | --- | --- |
> | `.nil?` | 16.69% | 31340 |
> | `T.must` | 23.89% | 74742 |
>
> From this, we see that
>
> - `T.must` is in 1.43x more files than `.nil?`
> - `T.must` occurs 2.38x more often than `.nil?`
>
>
> # Naming
>
> I prefer `must!` because it is what the method in Sorbet is already calle=
d.
>
> I am open to naming suggestions. Please provide reasoning.
>
>
> # Discussion
>
> In the above example, I used `T.must` twice. An alternative way to have
> written that would have been using save navigation:
>
> ```ruby
> T.must(task&.mailing_params).fetch('template_context')
> ```
>
> This works as well. The proposed `.must!` method works just as well when
> chaining methods with safe navigation:
>
> ```ruby
> task&.mailing_params.must!.fetch('template_context')
> ```
>
> However, there is still merit in using `T.must` (or `.must!`) twice=E2=80=
=94it
> calls out that the programmer
> intended neither location to be `nil`. In fact, if this method had been
> chained across multiple lines,
> the backtrace would include line numbers saying specifically **which**
> `.must!` failed:
>
>
> ```ruby
> task.must!
> .mailing_params.must!
> .fetch('template_context')
> ```
>
>
>
>
> --
> https://bugs.ruby-lang.org/
>
> Unsubscribe: <mailto:ruby-core-request@ruby-lang.org?subject=3Dunsubscrib=
e>
> <http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>
>
--=20
Austin Ziegler =E2=80=A2 halostatue@gmail.com =E2=80=A2 austin@halostatue.c=
a
http://www.halostatue.ca/ =E2=80=A2 http://twitter.com/halostatue
Unsubscribe: <mailto:ruby-core-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>