8 ms·
Coming from a language with functions as first class objects, blocks felt a bit limited to me, because it feels as if you almost have functions but not really,
by torben-friis 1y ago
Coming from a language with functions as first class objects, blocks felt a bit limited to me, because it feels as if you almost have functions but not really, and they get inputted by a back door. Used for example to:
let isLarge = a => a>100;
numbers.filter(isLarge)
Blocks let you do the same but without extracting the body as cleanly. Maybe it’s a chronological issue, where Ruby was born at a time when the above wasn’t commonplace?
>When you write 5.times { puts “Hello” }, you don’t think “I’m calling the times method and passing it a block.” You think “I’m doing something 5 times.”
I’m of two minds about this.
On the one hand, I do agree that aesthetically Ruby looks very clean and pleasing. On the other, I always feel like the mental model I have about a language is usually “dirtied” to improve syntax.
The value 5 having a method, and that method being an iterator for its value, is kinda weird in any design sense and doesn’t seem to fix any architectural order you might expect, it’s just there because the “hack” results in pretty text when used.
These magical tricks are everywhere in the language with missing_method and the like, and I guess there’s a divide between programmers’ minds when some go “oh that’s nice” and don’t care how the magic is done, and others are naturally irked by the “clever twists”.
- WJW 1y ago> The value 5 having a method, and that method being an iterator for its value, is kinda weird in any design sense and doesn’t seem to fix any architectural order you might expect, it’s just there because the “hack” results in pretty text when used. I don't think this is particularly weird, in Ruby at least. The language follows object orientation to its natural conclusion, which is that everything is an object, always. There is no such thing as "just data" in Ruby, because everything is an object. Even things that would just be an `int` in most other languages are actually objects, and so they have methods. The `times` method exists on the Integer classes because doing something exactly an integer number of times happens a lot in practice.
- torben-friis 1y agoI don’t have an issue with the “everything’s an object” part, because it _is_ consistent, even though it gets a bit trippy when classes are objects as well and they are implementation of a Class class which is an implementation of itself (trickery again!). The issue is more with this part: >The `times` method exists on the Integer classes because doing something exactly an integer number of times happens a lot in practice. It is practical, but it breaks the conceptual model in that it is a hard sell that “times” is a property over the “5” object. The result is cleaner syntax, I know, but there is something in these choices that still feels continually “hacky/irky” to me.
- WJW 1y agoPerhaps I've been doing Ruby for too long, but it's still not that weird to me. The quantity "5" is very abstract without anything to have "5" of. That is why "5.days" and "5.times" exist, among others. Mathematically it makes just as much sense to start with the amount and add the unit later than it does to start with the unit and add the amount later (ie like `time_to_wait = SECONDS_IN_A_DAY * 5` as you might do in some other languages).
- torben-friis 1y agoMaybe it is clearer if I explain it in syntactic terms? In my mental model objects are nouns (described entities) and methods are verbs - actions over the noun. process.start() is the action of starting done by the the noun that is the process. It's not exactly a matter of naming, as some methods are not explicitly verbs, but there is almost always an implicit action there: word.to_string() clearly has the convert/transform implication, even if ommitted for brevity. I see no path where 5 is a noun and times the verb, nor any verb I can put there that makes it make sense. If you try to stick a verb (iterate?) it becomes clear that 5 is not the noun, the thing performing the iteration, but a complement - X iterates (5 times). Perhaps the block itself having a times object with 5 as an input would make it more standard to me (?). But I do understand that if something is extremely practical a purist/conceptual argument doesn't go very far.
- oezi 1y agoI have been doing Ruby for so long that it feels very natural to apply a method in this way on the instance. false.not applies the not method on the false instance in the same way that car.start in every OO language calls the start method on car as the receiver. So filter(list) feels just wrong when you are clearly filtering the list itself.
- nasmorn 1y agoAlthough I prefer Elixir currently I agree that ruby at least goes all the way in on OO and not having to remember which feature is implemented as a language syntax and what is just a method invocation is a strength not a weakness. It is different in other languages for historical performance reasons really.
- TOGoS 1y ago`5.times` is not so outlandish, though it would seem better for that to be in a `Loop` library or something (`Loop.times(5) { do some stuff }`). The `5.days` example that was posted somewhere else in this thread might be a better example. It is not, as far as I can tell, part of Ruby's core library, thank goodness, but it is the kind of thing the methods-as-hacks-for-syntax culture seems to encourage. My question being "why the heck should the number 5 know anything about days? What does 5.days even mean, given that there are various ways to interpret 'a day'?" This kind of bad design has made its way all over the place. The Java mocking libraries that my coworkers like to use are full of it. Long chains of method calls that appear like they're trying to 'look like English' but make no damn sense, so you have to dig into each method call and what kind of thing it returns to understand what this chain actually means.
- zem 1y agoyou can view it as serving the same role as universal function call syntax, the latter being a feature in some other languages that rewrites `a.f(x)` to `f(a, x)` if `a` doesn't have a method `f`. in ruby you can just add the method to `a`'s class directly, but the idea is the same - you're adding some user defined function that is closely related to the type of `a`, and you're using method call syntax because that reads nicely.
- layer8 1y agoWith similar justification, you could argue that blocks should have a method `times` taking an integer, because repeating a block an integer number of times happens a lot in practice. I’d even argue that it is conceptually closer to blocks than to integers, and therefore the method belongs on the block. Hence you’d write `{ puts “Hello” }.times 5`. But blocks are not objects in Ruby, so you can’t do that, and everything actually isn’t an object in Ruby. Also, it’s even more common to do something depending on whether a condition is true or false, but true and false in Ruby don’t have a method to (not) execute a block on them, and you use a non-OOP `if` instead, so what’s up with that?
- bashkiddie 1y ago> everything is an object, always. I beg to differ. What object does the method `puts` belong to? Why do you not call puts with its objects name? Ruby has a concept of mixins (Golang interfaces), these are not objects. Neither is `puts`
- kazinator 1y agoAnswerable by a few minutes of googling. Sort of: puts is a method which has a class: the Method class: irb(main):001:0> method(:puts) => #<Method: main.puts> irb(main):002:0> method(:puts).class => Method Everything being a confused muddle in Ruby, there is evidently some Kernel base class that is injected into every Object, and puts is a private method in that: irb(main):003:0> 3.puts() Traceback (most recent call last): 2: from /usr/bin/irb:11:in `<main>' 1: from (irb):3 NoMethodError (private method `puts' called for 3:Integer) The Method class of puts is a real class with methods and all: irb(main):004:0> method(:puts).class.methods => [:allocate, :superclass, :<=>, :<=, :>=, :==, :===, :autoload?, :autoload, :included_modules, :include?, :name, :ancestors, :attr, :attr_reader, :attr_writer, :attr_accessor, :instance_methods, :public_instance_methods, :protected_instance_methods, :private_instance_methods, :constants, :const_get, :const_set, :const_defined?, :class_variables, :remove_class_variable, :class_variable_get, :class_variable_set, :class_variable_defined?, :public_constant, :freeze, :inspect, :deprecate_constant, :private_constant, :const_missing, :singleton_class?, :prepend, :class_exec, :module_eval, :class_eval, :include, :<, :>, :remove_method, :undef_method, :alias_method, :protected_method_defined?, :module_exec, :method_defined?, :public_method_defined?, :to_s, :public_class_method, :public_instance_method, :define_method, :private_method_defined?, :private_class_method, :instance_method, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :tap, :instance_variable_get, :instance_variables, :method, :public_method, :singleton_method, :define_singleton_method, :public_send, :extend, :to_enum, :enum_for, :pp, :=~, :!~, :eql?, :respond_to?, :object_id, :send, :display, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :yield_self, :taint, :tainted?, :untrust, :untaint, :trust, :untrusted?, :methods, :frozen?, :protected_methods, :singleton_methods, :public_methods, :private_methods, :!, :equal?, :instance_eval, :instance_exec, :!=, :__send__, :__id__]
- judofyr 1y agoBlocks are fundamentally different from functions due to the control flow: `return` inside a block will return the outer method, not the block. `break` stops the whole method that was invoked. This adds some complexity in the language, but it means that it’s far more expressive. In Ruby you can with nothing but Array#each write idiomatic code which reads very similar to other traditional languages with loops and statements.
- oezi 1y agoYou are right on return (use next in a block), but break uses block scope.
- judofyr 1y agoMaybe I explained it a bit imprecise. I was trying to explain the following behavior: def foo p 1 yield p 2 end foo { break } This only prints "1" because the break stops the execution of the invoked method (foo).
- Mystery-Machine 1y agoWAT? I'm a 12+ years Ruby developer and I didn't know this.
- vidarh 1y agoMore specifically blocks (and "proc"'s) return from the defining scope. This is just a minor clarification, but it matters, because if you pass a block down from where it is defined, and the block calls "return" it will still not just exit from the method where it was called, but from the method where it was defined. This can sometimes be useful: A calling method can pass down a block or proc to control if/when it wants an early return. Basically Ruby has two types of closures: * A return in a lambda returns to the calling scope. So basically, it returns to after where the "call" method is invoked. * A return in a block or a proc returns from the scope in which it was defined (this is also why you get LocalJumpError if you try to return a block or a proc, but not a lambda to the method calling the one where the block or proc is defined). When you name a block, you get a Proc object, same as you get when you take the value of a lambda or proc. In practice, that blocks in MRI are not Proc objects already is just an implementation detail/optimisation. I have a long-standing hobby project to write a Ruby compiler, and there a "proc" and a bare block are implemented identically in the backend.
- jhbadger 1y agoIf you are familiar with a true object-oriented language like Smalltalk (rather than the watered-down form of OO in C++, Java, etc.), an integer like 5 having methods makes sense because it (like everything else) is an object. Objects in Ruby aren't just window dressing -- they are its core.
- ck45 1y agoBut then Ruby only goes half way, not unlike the "watered-down form" in your term. Why is `#times` a method of Integer, but `#if` (or `#ifTrue`) not a method of booleans like in Smalltalk? Ruby does the same cherry picking from Smalltalk like everybody else, just different cherries. When looking at Ruby, it feels like the simple examples are all nice and clean but then the weird details start to appear and the language feels more hacky than others (like Ned Flander's house in Simpsons S08E08).
- chao- 1y ago#if and #ifTrue are yours if you want them: class TrueClass def if = true def ifTrue = true end class FalseClass def if = false def ifTrue = false end true.if # => true false.if # => false
- wild_egg 1y agoIn Smalltalk those methods don't return `true`. They take a block and evaluate it if the boolean receiving the message (a > b) ifTrue: [ "do something" ] EDIT: to clarify what's happening there, `>` is a message sent to `a` that will result in a boolean. The True class and False class both understand the ifTrue: message and `True>>ifTrue:` executes the block whereas `False>>ifTrue:` just throws it away. There's no `if` keyword in the language. Control flow is done purely through polymorphism.
- chao- 1y agoI apologize for my lack of Smalltalk knowledge. As you can imagine, you can do similar in Ruby by defining ifTrue to accept a block, even adding ifTrue on other all objects and defining something similar: class TrueClass def ifTrue(&block) = block.call end class FalseClass def ifTrue(&block) = nil end class Object def ifTrue(&block) = block.call end class NilClass def ifTrue(&block) = nil end If ck45's core complaint was that this is not baked into the language, I will agree that it is less convenient for lack of a default.
- chao- 1y agoThe "aesthetically pleasing" aspect of blocks is not mutually exclusive with real, first-class functions! Ruby is really more functional than that. Ruby has both lambas and method objects (pulled from instances). For example, you can write: let isLarge = a => a>100; as a lambda and call via #call or the shorthand syntax .(): is_large = ->(a) { a > 100 } is_large.call(1000) # => true is_large.(1000) # => true I find the .() syntax a bit odd, so I prefer #call, but that's a personal choice. Either way, it mixes-and-matches nicely with any class that has a #call method, and so it allows nice polymorphic mixtures of lambdas and of objects/instances that have a method named 'call'. Also very useful for injecting behavior (and mocking behavior in tests). Additionally, you can even take a reference to a method off of an object, and pass them around as though they are a callable lambda/block: class Foo def bar = 'baz' end foo_instance = Foo.new callable_bar = foo_instance.method(:bar) callable_bar.call # => 'baz' This ability to pull a method off is useful because any method which receives block can also take a "method object" and be passed to any block-receiving method via the "block operator" of '&' (example here is passing an object's method to Array#map as a block): class UpcaseCertainLetters def initialize(letters_to_upcase) @letters_to_upcase = letters_to_upcase end def format(str) str.chars.map do |char| @letters_to_upcase.include?(char) ? char.upcase : char end.join end end upcase_vowels = UpcaseCertainLetters.new("aeiuo").method(:format) ['foo', 'bar', 'baz'].map(&upcase_vowels) # => ['fOO', 'bAr', 'bAz'] This '&' operator is the same as the one that lets you call instance methods by converting a symbol of a method name into a block for an instance method on an object: (0..10).map(&:even?) # => [true, false, true, false, true, false, true, false, true, false, true] And doing similar, but with a lambda: is_div_five = ->(num) { num % 5 == 0 } (0..10).map(&is_div_five) # => [true, false, false, false, false, true, false, false, false, false, true]
- torben-friis 1y agoThat is interesting! I haven't explored Procs much, since I use ruby for a shared codebase at work and I was originally a bit afraid of trying to push unidiomatic ideas in the codebase. In your experience, is it ok to use Procs for example for extraction of block methods for cleanliness in refactors? or would I hit any major roadblocks if I treated them too much like first-class functions? Also, is there any particular Rails convention to place collections of useful procs? Or does that go a bit against the general model?
- somewhereoutth 1y agoInterestingly, in the Lambda Calculus, where everything is a function, a standard representation for a natural number n (i.e. a whole number >= 0), is indeed a function that 'iterates' (strictly, folds/recurses) n times. E.g. 3: (f, x) => f(f(f(x)))
- vidarh 1y agoThis is your example in Ruby: isLarge = -> {|a| a > 100 } numbers.filter(&isLarge) Or you could replace the first line: isLarge = -> { _1 > 100 } Some people hate that syntax, though. I think for trivial predicates like this, it's fine.