Is Ruby's code block same as C#'s lambda expression?

后端 未结 3 1187
北恋
北恋 2020-12-13 07:01

Are these two essentially the same thing? They look very similar to me.

Did lambda expression borrow its idea from Ruby?

3条回答
  •  醉话见心
    2020-12-13 07:04

    C# vs. Ruby

    Are these two essentially the same thing? They look very similar to me.

    They are very different.

    First off, lambdas in C# do two very different things, only one of which has an equivalent in Ruby. (And that equivalent is, surprise, lambdas, not blocks.)

    In C#, lambda expression literals are overloaded. (Interestingly, they are the only overloaded literals, as far as I know.) And they are overloaded on their result type. (Again, they are the only thing in C# that can be overloaded on its result type, methods can only be overloaded on their argument types.)

    C# lambda expression literals can either be an anonymous piece of executable code or an abstract representation of an anonymous piece of executable code, depending on whether their result type is Func / Action or Expression.

    Ruby doesn't have any equivalent for the latter functionality (well, there are interpreter-specific non-portable non-standardized extensions). And the equivalent for the former functionality is a lambda, not a block.

    The Ruby syntax for a lambda is very similar to C#:

    ->(x, y) { x + y }           # Ruby
    (x, y) => { return x + y; } // C#
    

    In C#, you can drop the return, the semicolon and the curly braces if you only have a single expression as the body:

    ->(x, y) { x + y }  # Ruby
    (x, y) => x + y    // C#
    

    You can leave off the parentheses if you have only one parameter:

    -> x { x }  # Ruby
    x => x     // C#
    

    In Ruby, you can leave off the parameter list if it is empty:

    -> { 42 }  # Ruby
    () => 42  // C#
    

    An alternative to using the literal lambda syntax in Ruby is to pass a block argument to the Kernel#lambda method:

    ->(x, y) { x + y }
    lambda {|x, y| x + y } # same thing
    

    The main difference between those two is that you don't know what lambda does, since it could be overridden, overwritten, wrapped or otherwise modified, whereas the behavior of literals cannot be modified in Ruby.

    In Ruby 1.8, you can also use Kernel#proc although you should probably avoid that since that method does something different in 1.9.

    Another difference between Ruby and C# is the syntax for calling a lambda:

    l.()  # Ruby
    l()  // C#
    

    I.e. in C#, you use the same syntax for calling a lambda that you would use for calling anything else, whereas in Ruby, the syntax for calling a method is different from the syntax for calling any other kind of callable object.

    Another difference is that in C#, () is built into the language and is only available for certain builtin types like methods, delegates, Actions and Funcs, whereas in Ruby, .() is simply syntactic sugar for .call() and can thus be made to work with any object by just implementing a call method.

    procs vs. lambdas

    So, what are lambdas exactly? Well, they are instances of the Proc class. Except there's a slight complication: there are actually two different kinds of instances of the Proc class which are subtly different. (IMHO, the Proc class should be split into two classes for the two different kinds of objects.)

    In particular, not all Procs are lambdas. You can check whether a Proc is a lambda by calling the Proc#lambda? method. (The usual convention is to call lambda Procs "lambdas" and non-lambda Procs just "procs".)

    Non-lambda procs are created by passing a block to Proc.new or to Kernel#proc. However, note that before Ruby 1.9, Kernel#proc creates a lambda, not a proc.

    What's the difference? Basically, lambdas behave more like methods, procs behave more like blocks.

    If you have followed some of the discussions on the Project Lambda for Java 8 mailinglists, you might have encountered the problem that it is not at all clear how non-local control-flow should behave with lambdas. In particular, there are three possible sensible behaviors for return (well, three possible but only two are really sensible) in a lambda:

    • return from the lambda
    • return from the method the lambda was called from
    • return from the method the lambda was created in

    That last one is a bit iffy, since in general the method will have already returned, but the other two both make perfect sense, and neither is more right or more obvious than the other. The current state of Project Lambda for Java 8 is that they use two different keywords (return and yield). Ruby uses the two different kinds of Procs:

    • procs return from the calling method (just like blocks)
    • lambdas return from the lambda (just like methods)

    They also differ in how they handle argument binding. Again, lambdas behave more like methods and procs behave more like blocks:

    • you can pass more arguments to a proc than there are parameters, in which case the excess arguments will be ignored
    • you can pass less arguments to a proc than there are parameters, in which case the excess parameters will be bound to nil
    • if you pass a single argument which is an Array (or responds to to_ary) and the proc has multiple parameters, the array will be unpacked and the elements bound to the parameters (exactly like they would in the case of destructuring assignment)

    Blocks: lightweight procs

    A block is essentially a lightweight proc. Every method in Ruby has exactly one block parameter, which does not actually appear in its parameter list (more on that later), i.e. is implicit. This means that on every method call you can pass a block argument, whether the method expects it or not.

    Since the block doesn't appear in the parameter list, there is no name you can use to refer to it. So, how do you use it? Well, the only two things you can do (not really, but more on that later) is call it implicitly via the yield keyword and check whether a block was passed via block_given?. (Since there is no name, you cannot use the call or nil? methods. What would you call them on?)

    Most Ruby implementations implement blocks in a very lightweight manner. In particular, they don't actually implement them as objects. However, since they have no name, you cannot refer to them, so it's actually impossible to tell whether they are objects or not. You can just think of them as procs, which makes it easier since there is one less different concept to keep in mind. Just treat the fact that they aren't actually implemented as blocks as a compiler optimization.

    to_proc and &

    There is actually a way to refer to a block: the & sigil / modifier / unary prefix operator. It can only appear in parameter lists and argument lists.

    In a parameter list, it means "wrap up the implicit block into a proc and bind it to this name". In an argument list, it means "unwrap this Proc into a block".

    def foo(&bar)
    end
    

    Inside the method, bar is now bound to a proc object that represents the block. This means for example that you can store it in an instance variable for later use.

    baz(&quux)
    

    In this case, baz is actually a method which takes zero arguments. But of course it takes the implicit block argument which all Ruby methods take. We are passing the contents of the variable quux, but unroll it into a block first.

    This "unrolling" actually works not just for Procs. & calls to_proc on the object first, to convert it to a proc. That way, any object can be converted into a block.

    The most widely used example is Symbol#to_proc, which first appeared sometime during the late 90s, I believe. It became popular when it was added to ActiveSupport from where it spread to Facets and other extension libraries. Finally, it was added to the Ruby 1.9 core library and backported to 1.8.7. It's pretty simple:

    class Symbol
      def to_proc
        ->(recv, *args) { recv.send self, *args }
      end
    end
    
    %w[Hello StackOverflow].map(&:length) # => [5, 13]
    

    Or, if you interpret classes as functions for creating objects, you can do something like this:

    class Class
      def to_proc
        -> *args { new *args }
      end
    end
    
    [1, 2, 3].map(&Array) # => [[nil], [nil, nil], [nil, nil, nil]]
    

    Methods and UnboundMethods

    Another class to represent a piece of executable code, is the Method class. Method objects are reified proxies for methods. You can create a Method object by calling Object#method on any object and passing the name of the method you want to reify:

    m = 'Hello'.method(:length)
    m.() #=> 5
    

    or using the method reference operator .::

    m = 'Hello'.:length
    m.() #=> 5
    

    Methods respond to to_proc, so you can pass them anywhere you could pass a block:

    [1, 2, 3].each(&method(:puts))
    # 1
    # 2
    # 3
    

    An UnboundMethod is a proxy for a method that hasn't been bound to a receiver yet, i.e. a method for which self hasn't been defined yet. You cannot call an UnboundMethod, but you can bind it to an object (which must be an instance of the module you got the method from), which will convert it to a Method.

    UnboundMethod objects are created by calling one of the methods from the Module#instance_method family, passing the name of the method as an argument.

    u = String.instance_method(:length)
    
    u.()
    # NoMethodError: undefined method `call' for #
    
    u.bind(42)
    # TypeError: bind argument must be an instance of String
    
    u.bind('Hello').() # => 5
    

    Generalized callable objects

    Like I already hinted at above: there's not much special about Procs and Methods. Any object that responds to call can be called and any object that responds to to_proc can be converted to a Proc and thus unwrapped into a block and passed to a method which expects a block.

    History

    Did lambda expression borrow its idea from Ruby?

    Probably not. Most modern programming languages have some form of anonymous literal block of code: Lisp (1958), Scheme, Smalltalk (1974), Perl, Python, ECMAScript, Ruby, Scala, Haskell, C++, D, Objective-C, even PHP(!). And of course, the whole idea goes back to Alonzo Church's λ-calculus (1935 and even earlier).

提交回复
热议问题