2012-01-29 22 views
80

para las fibras que han conseguido ejemplo clásico: la generación de los números de Fibonacci¿Por qué necesitamos fibras

fib = Fiber.new do 
    x, y = 0, 1 
    loop do 
    Fiber.yield y 
    x,y = y,x+y 
    end 
end 

¿Por qué necesitamos Fibras aquí? Puedo volver a escribir esto con la misma Proc (cierre, en realidad)

def clsr 
    x, y = 0, 1 
    Proc.new do 
    x, y = y, x + y 
    x 
    end 
end 

Así

10.times { puts fib.resume } 

y

prc = clsr 
10.times { puts prc.call } 

devolverá sólo el mismo resultado.

¿Cuáles son las ventajas de las fibras? ¿Qué tipo de cosas puedo escribir con Fibras que no puedo hacer con lambdas y otras divertidas características de Ruby?

+4

El viejo ejemplo de fibonacci es simplemente el peor motivador posible ;-) Incluso hay una fórmula que puede usar para calcular _ cualquier número de fibonacci en O (1). – usr

+15

El problema no es sobre el algoritmo, sino sobre la comprensión de las fibras :) – fl00r

Respuesta

197

Las fibras son algo que probablemente nunca uses directamente en el código de nivel de aplicación. Son una primitiva de control de flujo que puedes utilizar para construir otras abstracciones, que luego usas en el código de nivel superior.

Probablemente el uso # 1 de las fibras en Ruby es implementar Enumerator s, que son una clase básica de Ruby en Ruby 1.9. Estos son increíblemente útil.

En Ruby 1.9, si llama a casi cualquier método de iterador en las clases principales, sin pasando un bloque, devolverá un Enumerator.

irb(main):001:0> [1,2,3].reverse_each 
=> #<Enumerator: [1, 2, 3]:reverse_each> 
irb(main):002:0> "abc".chars 
=> #<Enumerator: "abc":chars> 
irb(main):003:0> 1.upto(10) 
=> #<Enumerator: 1:upto(10)> 

Estos Enumerator s son objetos enumerables, y sus each métodos producen los elementos que habrían sido producidas por el método iterador original, si se hubiera llamado con un bloque. En el ejemplo que acabo de dar, el Enumerator devuelto por reverse_each tiene un método each que produce 3,2,1. El enumerador devuelto por chars produce "c", "b", "a" (y así sucesivamente). Pero, a diferencia del método iterador original, el enumerador puede también devolver los elementos uno por uno si se llama next en él en varias ocasiones:

irb(main):001:0> e = "abc".chars 
=> #<Enumerator: "abc":chars> 
irb(main):002:0> e.next 
=> "a" 
irb(main):003:0> e.next 
=> "b" 
irb(main):004:0> e.next 
=> "c" 

Es posible que haya oído hablar de "iteradores internos" y "iteradores externos" (un buen la descripción de ambos se da en el libro "Patrones de Diseño de la Banda de los Cuatro"). El ejemplo anterior muestra que los Enumeradores se pueden usar para convertir un iterador interno en uno externo.

Ésta es una manera de hacer sus propios encuestadores:

class SomeClass 
    def an_iterator 
    # note the 'return enum_for...' pattern; it's very useful 
    # enum_for is an Object method 
    # so even for iterators which don't return an Enumerator when called 
    # with no block, you can easily get one by calling 'enum_for' 
    return enum_for(:an_iterator) if not block_given? 
    yield 1 
    yield 2 
    yield 3 
    end 
end 

Vamos a intentarlo:

e = SomeClass.new.an_iterator 
e.next # => 1 
e.next # => 2 
e.next # => 3 

Espera un momento ... no nada parece extraño no? Usted escribió las declaraciones yield en an_iterator como código de línea recta, pero el Enumerator puede ejecutarlas de una a la vez. Entre llamadas al next, la ejecución de an_iterator está "congelada". Cada vez que llama al next, continúa corriendo hasta la siguiente declaración yield, y luego se "congela" nuevamente.

¿Puedes adivinar cómo se implementa esto? El enumerador ajusta la llamada al an_iterator en una fibra y pasa un bloque que suspende la fibra. De modo que cada vez que an_iterator cede al bloque, la fibra en la que se está ejecutando se suspende y la ejecución continúa en el hilo principal. La próxima vez que llame al next, pasa el control a la fibra, el bloque devuelve, y an_iterator continúa donde lo dejó.

Sería instructivo pensar en lo que se requeriría para hacer esto sin fibras. CADA clase que quisiera proporcionar iteradores internos y externos debería contener un código explícito para realizar un seguimiento del estado entre llamadas al next. Cada llamada al siguiente debería verificar ese estado y actualizarlo antes de devolver un valor. Con las fibras, podemos automáticamente convertir cualquier iterador interno a uno externo.

Esto no tiene que ver con la persistencia de las fibras, pero permítanme mencionar una cosa más que puede hacer con Enumerators: le permiten aplicar métodos Enumerable de orden superior a otros iteradores distintos de each. Piense en esto: normalmente todos los métodos enumerables, incluyendo map, select, include?, inject, y así sucesivamente, todos los trabajo en los elementos producidos por each. Pero, ¿y si un objeto tiene otros iteradores que no sean each?

irb(main):001:0> "Hello".chars.select { |c| c =~ /[A-Z]/ } 
=> ["H"] 
irb(main):002:0> "Hello".bytes.sort 
=> [72, 101, 108, 108, 111] 

Llamando al iterador con ningún bloque Devuelve un enumerador, y entonces usted puede llamar a otros métodos enumerables sobre eso.

Volviendo a las fibras, ¿ha utilizado el método take de Enumerable?

class InfiniteSeries 
    include Enumerable 
    def each 
    i = 0 
    loop { yield(i += 1) } 
    end 
end 

Si algo llama que each método, parece que nunca debe volver, ¿verdad? Mira esto:

InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 

No sé si esto utiliza fibras debajo del capó, pero podría. Las fibras se pueden utilizar para implementar listas infinitas y evaluación diferida de una serie. Para un ejemplo de algunos métodos perezosos definidos con Enumerators, he definido algunos aquí: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

También puede crear una instalación de corutina de uso general utilizando fibras. Nunca utilicé corutinas en ninguno de mis programas, pero es un buen concepto saberlo.

Espero que esto le dé una idea de las posibilidades. Como dije al principio, las fibras son una primitiva de control de flujo de bajo nivel. Permiten mantener múltiples "posiciones" de control de flujo dentro de su programa (como diferentes "marcadores" en las páginas de un libro) y cambiar entre ellas según lo desee. Como el código arbitrario puede ejecutarse en una fibra, puede llamar al código de un tercero en una fibra, y luego "congelarlo" y continuar haciendo otra cosa cuando vuelva a llamar al código que usted controla.

Imagine algo como esto: está escribiendo un programa de servidor que atenderá a muchos clientes. Una interacción completa con un cliente implica pasar por una serie de pasos, pero cada conexión es transitoria, y debe recordar el estado de cada cliente entre conexiones. (¿Suena como programación web?)

En lugar de almacenar explícitamente ese estado, y consultarlo cada vez que un cliente se conecta (para ver cuál es el próximo "paso" que deben hacer), puede mantener una fibra para cada cliente . Después de identificar al cliente, recuperaría su fibra y la reiniciaría. Luego, al final de cada conexión, suspenderías la fibra y la almacenarías nuevamente. De esta forma, podría escribir un código de línea recta para implementar toda la lógica para una interacción completa, incluidos todos los pasos (tal como lo haría naturalmente si su programa fuera ejecutado localmente).

Estoy seguro de que hay muchas razones por las cuales tal cosa puede no ser práctica (al menos por ahora), pero una vez más, solo estoy tratando de mostrarle algunas de las posibilidades. Quién sabe; ¡Una vez que obtenga el concepto, puede encontrar una aplicación totalmente nueva en la que nadie más haya pensado todavía!

+0

¡Gracias por su respuesta! Entonces, ¿por qué no implementan 'chars' u otros enumeradores con solo cierres? – fl00r

+0

@ fl00r, estoy pensando en agregar aún más información, pero no sé si esta respuesta ya es demasiado larga ... ¿quieres más? –

+0

¡Quiero! :) ¡Con gran placer! – fl00r

17

a diferencia de un dispositivos de cierre, que tienen un punto de entrada y de salida definido, las fibras pueden preservar su estado y de retorno (rendimiento) muchas veces:

f = Fiber.new do 
    puts 'some code' 
    param = Fiber.yield 'return' # sent parameter, received parameter 
    puts "received param: #{param}" 
    Fiber.yield #nothing sent, nothing received 
    puts 'etc' 
end 

puts f.resume 
f.resume 'param' 
f.resume 

grabados esto:

some code 
return 
received param: param 
etc 

La aplicación de este la lógica con otras características de rubí será menos legible.

Con esta característica, el uso de buenas fibras es hacer una programación cooperativa manual (como reemplazo de hilos). Ilya Grigorik tiene un buen ejemplo sobre cómo convertir una biblioteca asíncrona (eventmachine en este caso) en lo que parece una API síncrona sin perder las ventajas de la programación IO de la ejecución asincrónica. Aquí está el link.

+0

¡Gracias! Leo documentos, así que entiendo toda esta magia con muchas entradas y salidas dentro de fibra. Pero no estoy seguro de que esto haga la vida más fácil. No creo que sea una buena idea tratar de seguir todos estos currículos y rendimientos. Parece una ovillo que es difícil de desenredar. Así que quiero entender si hay casos en que este ovillo de fibras es una buena solución. Eventmachine es genial, pero no es el mejor lugar para entender las fibras, porque primero debes entender todas estas cosas del patrón del reactor. Así que creo que puedo entender el significado físico de las fibras en el ejemplo más simple – fl00r

Cuestiones relacionadas