From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on dcvr.yhbt.net X-Spam-Level: X-Spam-Status: No, score=-4.1 required=3.0 tests=AWL,BAYES_00, MAILING_LIST_MULTI,RCVD_IN_DNSWL_MED,SPF_HELO_NONE,SPF_PASS, UNPARSEABLE_RELAY shortcircuit=no autolearn=ham autolearn_force=no version=3.4.2 Received: from neon.ruby-lang.org (neon.ruby-lang.org [221.186.184.75]) by dcvr.yhbt.net (Postfix) with ESMTP id 205E71F66E for ; Mon, 24 Aug 2020 13:44:04 +0000 (UTC) Received: from neon.ruby-lang.org (localhost [IPv6:::1]) by neon.ruby-lang.org (Postfix) with ESMTP id 178FF12097C; Mon, 24 Aug 2020 22:43:31 +0900 (JST) Received: from xtrwkhkc.outbound-mail.sendgrid.net (xtrwkhkc.outbound-mail.sendgrid.net [167.89.16.28]) by neon.ruby-lang.org (Postfix) with ESMTPS id 3A062120978 for ; Mon, 24 Aug 2020 22:43:29 +0900 (JST) Received: by filterdrecv-p3iad2-86945d9569-cz9wf with SMTP id filterdrecv-p3iad2-86945d9569-cz9wf-18-5F43C41C-22 2020-08-24 13:43:56.307242781 +0000 UTC m=+330052.323376700 Received: from herokuapp.com (unknown) by geopod-ismtpd-1-1 (SG) with ESMTP id dWqZqB9nSiye_qv4IxqQNA for ; Mon, 24 Aug 2020 13:43:56.243 +0000 (UTC) Date: Mon, 24 Aug 2020 13:43:56 +0000 (UTC) From: mame@ruby-lang.org Message-ID: References: Mime-Version: 1.0 X-Redmine-MailingListIntegration-Message-Ids: 75513 X-Redmine-Project: ruby-master X-Redmine-Issue-Tracker: Feature X-Redmine-Issue-Id: 16986 X-Redmine-Issue-Author: ko1 X-Redmine-Issue-Assignee: matz X-Redmine-Sender: mame X-Mailer: Redmine X-Redmine-Host: bugs.ruby-lang.org X-Redmine-Site: Ruby Issue Tracking System X-Auto-Response-Suppress: All Auto-Submitted: auto-generated X-SG-EID: =?us-ascii?Q?EJh2gqwnyqXtd++xo=2FinyA1V0bXouTB4FkWnzNiKb49jGT+zJDgketmNn4pYXr?= =?us-ascii?Q?3chqPopLoh9TaulA=2FF2tv64csQByQPSPWWXN4pt?= =?us-ascii?Q?+KWeiD27k=2FGzodHyOAHxzTa6ZjRasxByE54ns2X?= =?us-ascii?Q?vB1URKeYLOmMA6zyi=2FzqUye3JINkkb3IyW+3vTQ?= =?us-ascii?Q?klfKmO+6oinefNJcKeAaA5+msh8LgGBvxs49GHa?= =?us-ascii?Q?7krku2VfNTPrChZos=3D?= To: ruby-core@ruby-lang.org X-ML-Name: ruby-core X-Mail-Count: 99682 Subject: [ruby-core:99682] [Ruby master Feature#16986] Anonymous Struct literal X-BeenThere: ruby-core@ruby-lang.org X-Mailman-Version: 2.1.15 Precedence: list Reply-To: Ruby developers List-Id: Ruby developers List-Unsubscribe: , List-Post: List-Help: List-Subscribe: , Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: ruby-core-bounces@ruby-lang.org Sender: "ruby-core" Issue #16986 has been updated by mame (Yusuke Endoh). Dan0042 (Daniel DeLorme) wrote in #note-37: > > I thought that this would be a useful substitute of symbol-key Hash (like JSON data) which we can read the values by `s.name` instead of `s[:name]`. > > This is the same thing stated in the Feature proposal, and I would also like this, and it seems a lot of other people here too. I thought so. Thus, I wanted to let all of you know what this proposal is *not*. BTW, the proposal includes the following note that states the problem: > Note > Unlike Hash literal syntax, this proposal only allows label: expr notation. No ${**h} syntax. > This is because if we allow to splat a Hash, it can be a vulnerability by splatting outer-input Hash. So, external input like JSON data is not the target of this proposal. Rather, this proposal is just a variant of Struct, which allows to omit the definition line: `Foo = Struct.new(...)`. > > Because method-name symbols are never GC'ed, so converting arbitrary external input to anonymous Struct is vulnerable against Symbol DoS. > > It should be possible to work around this. Let say we create an anonymous struct with `Struct(**json)`, instead of creating an actual `Struct.new` it could be something like `AnonStruct.new` where only _already-existing_ method-name symbols are created as methods, and the other keys are handled via method_missing. In my opinion, it is too hacky to include into the core. > > It creates a struct class for all unique set of keys. > > Assuming that the struct class can be garbage-collected when there are no longer any instances, I think this should not be a big problem? Theoretically, yes. But it will require much work. ---------------------------------------- Feature #16986: Anonymous Struct literal https://bugs.ruby-lang.org/issues/16986#change-87169 * Author: ko1 (Koichi Sasada) * Status: Open * Priority: Normal * Assignee: matz (Yukihiro Matsumoto) ---------------------------------------- # Abstract How about introducing anonymous Struct literal such as `${a: 1, b: 2}`? It is almost the same as `Struct.new(:a, :b).new(1, 2)`. # Proposal ## Background In many cases, people use hash objects to represent a set of values such as `person = {name: "ko1", country: 'Japan'}` and access its values through `person[:name]` and so on. It is not easy to write (three characters `[:]`!), and it easily introduces misspelling (`person[:nama]` doesn't raise an error). If we make a `Struct` object by doing `Person = Struct.new(:name, :age)` and `person = Person.new('ko1', 'Japan')`, we can access its values through `person.name` naturally. However, it costs coding. And in some cases, we don't want to name the class (such as `Person`). Using `OpenStruct` (`person = OpenStruct.new(name: "ko1", country: "Japan")`), we can access it through `person.name`, but we can extend the fields unintentionally, and the performance is not good. Of course, we can define a class `Person` with attr_readers. But it takes several lines. To summarize the needs: * Easy to write * Doesn't require declaring the class * Accessible through `person.name` format * Limited fields * Better performance ## Idea Introduce new literal syntax for an anonymous Struct such as: `${ a: 1, b: 2 }`. Similar to Hash syntax (with labels), but with `$` prefix to distinguish. Anonymous structs which have the same member in the same order share their class. ```ruby s1 = ${a: 1, b: 2, c: 3} s2 = ${a: 1, b: 2, c: 3} assert s1 == s2 s3 = ${a: 1, c: 3, b: 2} s4 = ${d: 4} assert_equal false, s1 == s3 assert_equal false, s1 == s4 ``` ## Note Unlike Hash literal syntax, this proposal only allows `label: expr` notation. No `${**h}` syntax. This is because if we allow to splat a Hash, it can be a vulnerability by splatting outer-input Hash. Thanks to this spec, we can specify anonymous Struct classes at compile time. We don't need to find or create Struct classes at runtime. ## Implementatation https://github.com/ruby/ruby/pull/3259 # Discussion ## Notation Matz said he thought about `{|a: 1, b: 2 |}` syntax. ## Performance Surprisingly, Hash is fast and Struct is slow. ```ruby Benchmark.driver do |r| r.prelude <<~PRELUDE st = Struct.new(:a, :b).new(1, 2) hs = {a: 1, b: 2} class C attr_reader :a, :b def initialize() = (@a = 1; @b = 2) end ob = C.new PRELUDE r.report "ob.a" r.report "hs[:a]" r.report "st.a" end __END__ Warming up -------------------------------------- ob.a 38.100M i/s - 38.142M times in 1.001101s (26.25ns/i, 76clocks/i) hs[:a] 37.845M i/s - 38.037M times in 1.005051s (26.42ns/i, 76clocks/i) st.a 33.348M i/s - 33.612M times in 1.007904s (29.99ns/i, 87clocks/i) Calculating ------------------------------------- ob.a 87.917M i/s - 114.300M times in 1.300085s (11.37ns/i, 33clocks/i) hs[:a] 85.504M i/s - 113.536M times in 1.327850s (11.70ns/i, 33clocks/i) st.a 61.337M i/s - 100.045M times in 1.631064s (16.30ns/i, 47clocks/i) Comparison: ob.a: 87917391.4 i/s hs[:a]: 85503703.6 i/s - 1.03x slower st.a: 61337463.3 i/s - 1.43x slower ``` I believe we can speed up `Struct` similarly to ivar accesses, so we can improve the performance. BTW, OpenStruct (os.a) is slow. ``` Comparison: hs[:a]: 92835317.7 i/s ob.a: 85865849.5 i/s - 1.08x slower st.a: 53480417.5 i/s - 1.74x slower os.a: 12541267.7 i/s - 7.40x slower ``` For memory consumption, `Struct` is more lightweight because we don't need to keep the key names. ## Naming If we name an anonymous class, literals with the same members share the name. ```ruby s1 = ${a:1} s2 = ${a:2} p [s1, s2] #=> [#, #] A = s1.class p [s1, s2] #=> [#, #] ``` Maybe that is not a good behavior. -- https://bugs.ruby-lang.org/