ruby - ejemplos - ¿Por qué necesitamos fibras?
meta tags seo (2)
Para Fibras tenemos un ejemplo clásico: generación de números de Fibonacci
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 reescribir esto con el mismo Proc (cierre, en realidad)
def clsr
x, y = 0, 1
Proc.new do
x, y = y, x + y
x
end
end
Asi que
10.times { puts fib.resume }
y
prc = clsr
10.times { puts prc.call }
devolverá el mismo resultado.
Entonces, ¿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?
A diferencia de los cierres, que tienen un punto de entrada y salida definido, las fibras pueden preservar su estado y regresar (ceder) 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
imprime esto:
some code
return
received param: param
etc
La implementación de esta lógica con otras características de ruby será menos legible.
Con esta característica, el buen uso de las fibras es hacer una programación cooperativa manual (como reemplazo de los hilos). Ilya Grigorik tiene un buen ejemplo sobre cómo convertir una biblioteca asíncrona ( eventmachine
en este caso) en lo que parece ser una API síncrona sin perder las ventajas de la programación IO de la ejecución asincrónica. Aquí está el link .
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 útiles.
En Ruby 1.9, si llama a casi cualquier método de iterador en las clases principales, sin pasar 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
son objetos enumerables, y each
proporciona los elementos que habría obtenido el método del iterador original, de haber sido invocado con un bloque. En el ejemplo que acabo de dar, el Enumerator devuelto por reverse_each
tiene each
método que produce 3,2,1. El enumerador devuelto por chars
produce "c", "b", "a" (etc.). PERO, a diferencia del método del iterador original, el Enumerator también puede devolver los elementos uno por uno si llama al next
repetidamente:
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" e "iteradores externos" (una buena descripción de ambos se da en el libro "Patrones de diseño de" Banda de cuatro "). El ejemplo anterior muestra que los Enumeradores se pueden usar para convertir un iterador interno en uno externo.
Esta es una forma de crear sus propios enumeradores:
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 minuto ... ¿algo parece extraño allí? Usted escribió las declaraciones de yield
en an_iterator
como código de línea recta, pero el Enumerador puede ejecutarlas de a una por vez . Entre las llamadas a la next
, la ejecución de an_iterator
está "congelada". Cada vez que llama a next
, continúa corriendo hasta la siguiente declaración de yield
, y luego se "congela" nuevamente.
¿Puedes adivinar cómo se implementa esto? El an_iterator
envuelve la llamada a un an_iterator
en una fibra y pasa un bloque que suspende la fibra . Por lo tanto, cada vez que an_iterator
cede al bloque, la fibra en la que se ejecuta se suspende y la ejecución continúa en el hilo principal. La próxima vez que llame a next
, pasa el control a la fibra, el bloque regresa y un 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 hacer un seguimiento del estado entre las llamadas a la next
. Cada llamada al siguiente debería verificar ese estado y actualizarlo antes de devolver un valor. Con las fibras, podemos convertir automáticamente 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 los Enumeradores: le permiten aplicar métodos Enumerable de orden superior a otros iteradores distintos de each
. Piénselo: normalmente, todos los métodos Enumerable, incluido map
, select
, include?
, inject
, etc., todo el trabajo sobre los elementos producidos por each
. Pero, ¿qué pasa si un objeto tiene otros iteradores distintos de 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]
Llamar al iterador sin bloque devuelve un Enumerador y luego puede llamar a otros métodos Enumerable.
Volviendo a las fibras, ¿has usado el método de take
de Enumerable?
class InfiniteSeries
include Enumerable
def each
i = 0
loop { yield(i += 1) }
end
end
Si algo llama a each
método, parece que nunca debería 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 construir una instalación de coroutine de uso general utilizando fibras. Nunca utilicé corutinas en ninguno de mis programas, pero es un buen concepto saberlo.
Espero que esto te 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 servirá 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 verificarlo cada vez que un cliente se conecta (para ver cuál es el siguiente "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 de nuevo solo estoy tratando de mostrarte 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!