Ruby: callcc

June 3rd, 2009 — 04:04 pm

callcc, es la abreviatura de call-with-current-continuation, algo así como un goto con esteroides.

En el rdoc nos encontramos con la siguiente definición:

callcc {|cont| block } => obj

Generates a Continuation object, which it passes to the associated block. Performing a cont.call will cause the callcc to return (as will falling through the end of the block). The value returned by the callcc is the value of the block, or the value passed to cont.call. See class Continuation for more details. Also see Kernel::throw for an alternative mechanism for unwinding a call stack.

Un tanto críptico, ¿No?. Basicamente, callcc almacena la dirección de memoria y el contexto (en un objeto Continuation) de donde se le llamo. Sigue siendo raro,¿No?

Mejor un ejemplo. Sea el siguiente metodo:

 
counter = 0
 
def foo
exit if callcc{|$cont| } == 3
puts "hello world"
end
 
foo
 
$cont.call(counter += 1)

(Si ya se que las variables globales son malignas : ( )

Ejecutando el ejemplo:

madtrick::madtrick::$ruby test3.rb
hello world
hello world
hello world

Como se puede apreciar, se imprimen tres “hello world” con sólo una llamada al método foo.

¿Qué podemos extraer del ejemplo anterior?

  • Cada llamada a la continuación ($cont) nos vuelve a llevar a linea donde se definió.
  • Si en la llamada a la continuación pasamos algun parámetro, este es devuelto por callcc

Tambien podríamos utilizar los ejemplos del post anterior (Tail Call Optimization) para explicar el funcionamiento de callcc:

Calculo del factorial de un número:

class TCOTest
  # tail-recursive factorial
  def fact( n, acc = 1 )
    if n < 2 then
      acc
    else
       fact( n-1, n*acc ) 
    end
  end
 
  def fact_cc( n, acc = 1 )
    cont = callcc{|cont| cont}
    if n < 2 then
      acc
    else
       n, acc = n-1, n*acc
       cont.call cont 
    end
  end
 
  # length of factorial
  def fact_size( n )
    fact( n ).size
  rescue
    $!
  end
 
 
  # length of factorial
  def fact_size_cc( n )
    fact_cc( n ).size
  rescue
    $!
  end  
end

Si lo probamos en el irb vemos que fact_size_cc no da ningún tipo de problema mientras que la función fact_size (sí, por carecer Ruby de TCO) ya que nos quedamos sin espacio en la stack.

...
irb(main):009:0> t.fact_size 10000
=> #<SystemStackError: stack level too deep>
irb(main):010:0> t.fact_size_cc 10000
=> 14808

El otro ejemplo del post anterior, la función de Fibonacci:

En el ejemplo anterior, utilizábamos la palabra reservada redo junto con un método definido ad-hoc para poder realizar el cálculo:

  define_method(:acc) do |i, n, result|
    if i == -1
      result
    else
      i, n, result = i - 1, n + result, n
      redo
    end
 
def fib_redo(i)
   acc i,1,0
end

Pero tambien se podria haber hecho de esta otra forma:

def fib_call_cc(i, n = 1, result = 0)
  cont = callcc{|cont| cont}
  if i == -1
    result
  else
    i, n, result = i - 1, n + result, n
    cont.call cont
  end
end

En el ejemplo anterior se puede apreciar como el contexto se mantiene. En este caso para los valores i,n y result.

Si ejecutamos un simple benchmark (para n en 50000) para comparar redo vs callcc, obtenemos los siguiente:

 
madtrick::madtrick::$ruby test.rb 
Time for fib_redo:      0.390000   0.010000   0.400000 (  0.403986)
Time for fib_call_cc:   0.500000   0.000000   0.500000 (  0.519504)

redo ( 0.403986) es un poco más rápido que callcc ( 0.519504). Echándole un vistazo rapido al codigo de la máquina virtual de Ruby se entiende rápidamente el porque. redo esta implentado utilizando goto’s mientras que callcc es una llamada a una función que se encarga de guardar toda la información necesaria en un objeto Continuation, por lo que necesita más tiempo.

Para mas información sobre callcc, su funcionamiento y los continuations:

Comment » | programacion, ruby

Ruby: TCO (Tail Call Optimization)

June 1st, 2009 — 11:21 pm

Leyendo el libro de Programming Erlang descubrí un concepto que no conocia: funciones tail-recursive.

En esta pagina lo explican bastante bien (entre otros tipos de recursividad), pero basicamente una función tail-recursive es aquella que no tiene ninguna operación pendiente de ejecutar tras la llamada recursiva. En estas funciones, la llamada recursiva puede ser substituida simplemente por un salto al comienzo de la función que se llama, con lo que se ahorra espacio en el stack. De esto ultimo se encarga el compilador y es lo que se conoce como Tail Call Optimization (TCO a partir de ahora).

Así que aparentemente la TCO mola, ¿No?

El problema es que Ruby no tiene implementada TCO en su maquina virtual con lo que independientemente de como estructuremos nuestras funciones recursivas, siempre estaremos limitados por el tamaño de nuestra Stack.

Pero siempre hay solución para todo y con Ruby, más. En ésta página dan dos soluciones para tener una pseudo-TCO.

Copio y pego la primera solución:

class Class
  # Sweet stuff!
  def tailcall_optimize( *methods )
    methods.each do |meth|
      org = instance_method( meth )
      define_method( meth ) do |*args|
        if Thread.current[ meth ]
          throw( :recurse, args )
        else
          Thread.current[ meth ] = org.bind( self )
          result = catch( :done ) do
            loop do
              args = catch( :recurse ) do
                throw( :done, Thread.current[ meth ].call( *args ) )
              end
            end
          end
          Thread.current[ meth ] = nil
          result
        end
      end
    end
  end
end
 
class TCOTest
  # tail-recursive factorial
  def fact( n, acc = 1 )
    if n < 2 then acc else fact( n-1, n*acc ) end
  end
 
  # length of factorial
  def fact_size( n )
    fact( n ).size
  rescue
    $!
  end   
end
 
t = TCOTest.new
 
# normal method
puts t.fact_size( 10000 )  # => stack level too deep
 
# enable tail-call optimization
class TCOTest
  tailcall_optimize :fact
end
 
# tail-call optimized method
puts t.fact_size( 10000 )  # => 14808

Esta solución consiste en jugar con catch’s,throw’s y Thread.current para relanzar el mismo método una y otra vez sin tener que crear un nuevo frame en el Stack, ya que el codigo del metodo se encuentra definido en un bloque y por tanto reutiliza el codigo.

La segunda opción:

def fib(i, n = 1, result = 0)
  if i == -1
    result
  else
    i, n, result = i - 1, n + result, n
    redo
  end
end
 
fib(10000)

Esta solución consiste en hacer uso de la palabra redo la cual reinicia la ejecución de cualquier loop o iterador. El problema con redo es que, como acabo de decir, solo se puede utilizar en loops o iteradores y el método fib anterior no es ninguno de ellos. Por lo que el autor de la página de que se extrajeron las soluciones dice:

Unfortunately, “The redo statement restarts the current iteration of a loop or iterator”, so it only throws a LocalJumpError

ruby_spartan1

Gracias señor espartano.

Así es, esto es Ruby y siempre hay un workaround para todo. En este caso, consiste en crear un nuevo método cuyo cuerpo este contenido en un bloque.

define_method(:acc) do |i, n, result|
  if i == -1
    result
  else
    i, n, result = i - 1, n + result, n
    redo
  end
end
 
def fib(i)
  acc(i, 1, 0)
end
 
fib(10000)  # Yeah!

Y ya podemos utilizar redo. Fin!!

Para mas información, hay un thread en esta lista de correo en el que debaten sobre este tema: Ruby tail recursion.

Comment » | programacion, ruby

Ruby: Expirar los mensajes de Starling en 3 comodos pasos

March 20th, 2009 — 06:12 pm

Si te pasa como a mi, que quieres almacenar mensajes en el starling pero que algunos de ellos lleven “fecha de caducidad“, sigue los siguiente pasos.

  • Paso 1 : Abrir en un editor el fichero handler.rb perteneciente a la gema starling-starling.
  • Paso 2 : En el método Handler#set_data añadir la siguiente linea justo antes de hacer el pack para generar la variable internal_data.
  •   ...
      expiry = Time.now.to_i + expiry.to_i unless expiry.to_i == 0
      ...
  • Paso 3 : Guarda y re-arranca el starling.Todo debería de ir como la seda.

Con esto, lo único que hemos hecho es corregir un pequeño bug que hay en Handler#get ya que en este método solo se devuelve un mensaje si su expiry es 0 o si expiry es mayor que now (now en este contexto es Time.now.to_i ). El fallo viene de comparar a now (valor unix time) con expiry el cual es solo un offset desde el momento en el que decidimos guardar un mensaje en starling.

  ...
  break if expiry == 0 || expiry >= now
 
  @expiry_stats[key] += 1
  expiry, data = nil
  ...

Con la linea que añadimos al método Handler#set_data forzamos que la comparación sea entre dos valores unix time.Uno refiriéndose al momento actual (now) y otro al momento en el que expirara el mensaje (la suma de Time.now.to_i en el momento del almacenarlo y el tiempo de vida del mensaje).

2 comments » | programacion, ruby

Mac & Git & Ssh : Un poquillo de scripting

February 26th, 2009 — 10:54 pm

Planteemos el siguiente problema:

Tenemos que hacer push’es y pull’es a un repositorio git (mediante ssh).

Sencillo, ¿no?.Ahora tengamos en cuenta las siguientes consideraciones:

  1. Estas operaciones las vamos a realizar tanto desde nuestra casa (a través de internet) como desde la red donde se encuentra el repositorio, siendo la dirección del repositorio – git.frusfrusfrus.net, la misma en ambos casos.
  2. Por culpa de las hadas y de los proxies reversos, si las operaciones la realizamos desde casa el puerto sera el 2280 mientras que si las operaciones las realizamos desde la oficina el puerto será el 22.
  3. Queremos que cualquier tipo de cambio en la configuración sea automático

La consideración tres es el motivo de este post, ya que la dos (la unica que plantea algún problema) se podria resolver con un parámetro en la linea de comandos.Es más cómodo  no tener que preocuparse ni de donde estamos, ni tener que acordarse de como era la estructura del comando.

Mi primera opción fue la de crear un script que se ejecutase cada vez que encendiera el ordenador, para lo cual ya le habia hechado un vistazo a esta documentación. Pero como David tuvo bien a decirme, quizás seria mejor hacerlo siempre que la interfaz de red se levantara.Realmente esto está mucho mejor ya que cubria un abanico de opciones más amplio (volver de hibernación, desactivar y activar de nuevo la tarjeta de red…) que la idea de crear  un Startup item.

¿Y como co%&o se yo cuando se levanta o se cae una interfaz de red en Mac OS X?

…google…google…google…google

Resulta que en Mac OS X  la gestión de las interfaces de red (entre otras muchas más cosas) las lleva un demonio: configd. Este demonio monitoriza el siguiente fichero, /Library/Preferences/SystemConfiguration/preferences.plist. Configd reacciona ante cualquier cambio que se refleje en este fichero, derivando a quien corresponda  la tarea de manejar el evento.

Si el evento tiene que ver con la red del sistema, configd hace entrar en juego al  Kicker.bundle – que se puede encontrar en /System/library/SystemConfiguration/Kicker.bundle.Lo interesante de este bundle es su fichero de configuración: Kicker.xml en el cual se especifica ante que eventos reaccionar y lo que es más importante, como.

Este es mi fichero Kicker.xml original

<pre lang="xml">
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
	<dict>
		<key>execCommand</key>
		<string>$BUNDLE/Contents/Resources/enable-network</string>
		<key>execUID</key>
		<integer>0</integer>
		<key>keys</key>
		<array>
			<string>State:/Network/Global/IPv4</string>
		</array>
		<key>name</key>
		<string>enable-network</string>
	</dict>
 
	<dict>
		<key>keys</key>
		<array>
			<string>State:/Network/Global/DNS</string>
			<string>State:/Network/Global/IPv4</string>
			<string>State:/Network/Global/IPv6</string>
			<string>State:/Network/Global/NetInfo</string>
		</array>
		<key>name</key>
		<string>network_change</string>
		<key>postName</key>
		<string>com.apple.system.config.network_change</string>
	</dict>
	<dict>
		<key>execCommand</key>
		<string>/usr/sbin/AppleFileServer</string>
		<key>execUID</key>
		<integer>0</integer>
		<key>keys</key>
		<array>
			<string>daemon:AppleFileServer</string>
		</array>
		<key>name</key>
		<string>AppleFileServer</string>
	</dict>
</array>
</plist>

La primera sección dict , indica que se debe de ejecutar el script enable-network cada vez que se reciba un evento de tipo State:/Network/Global/IPv4 lo cual puede indicar que la interfaz se levantó y ha obtenido una ip, que se ha realizado un cambio de ip, que la interfaz se ha desconectado…etc.

La segunda sección dict indica que ante todos esos eventos : State:/Network/Global/IPv4 , State:/Network/Global/IPv6 … se va a emitir una notificación con el nombre com.apple.system.config.network_change.Decir que principalmente quien hará uso de este notificación sera lookupd, como se puede observar en este extracto del sytem.log:

Feb 26 17:36:52 madtrick configd[39]: posting notification com.apple.system.config.network_change
Feb 26 17:36:52 madtrick lookupd[538]: lookupd (version 369.8) starting - Thu Feb 26 17:36:52 2009
...
Feb 26 19:46:16 madtrick configd[39]: posting notification com.apple.system.config.network_change
Feb 26 19:46:16 madtrick lookupd[667]: lookupd (version 369.8) starting - Thu Feb 26 19:46:16 2009

La tercera y ultima sección no es relevante a lo que estamos hablando, así que nada que comentar.

Bien.Llegados a este punto se me plantean dos opciones:

  • Primera. Crear una nueva entrada en el fichero Kicker.xml para que un script se ejecute ante cualquier evento de tipo  State:/Network/Global/IPv4.
  • Segunda. Capturar la notificacion com.apple.system.config.network_change y actuar en consecuencia.

Aparentemente la más interesante es la segunda.Es menos intrusiva y mas versatil,peeero tambien bastante mas complicada de afrontar de primeras.Por ello queda aplazada a la espera de que algún dia decida leer esto y  me plantee hacerlo.

Así que nos queda la primera opcion, ejecutar un script ante cada evento.

OJO: ANTES DE EDITAR Kicker.xml, HAZ UNA COPIA DE SEGURIDAD

Asi que nos ponemos manos a la obra y añadimos la siguiente sección dict al fichero Kicker.xml

	<dict>
		<key>execCommand</key>
		<string>/usr/bin/ssh-config</string>
		<key>execUID</key>
		<integer>501</integer>
		<key>keys</key>
		<array>
		<string>State:/Network/Global/IPv4</string>
		</array>
		<key>name</key>
		<string>ssh-config</string>
	</dict>

Donde /usr/bin/ssh-config es el script que se ejecutara ante cada cambio que afecte a la pila IPv4.

Para que los cambios tengan efecto, necesitamos matar el demonio configd y luego volver a levantarlo.

madtrick::madtrick::$sudo killall configd
madtrick::madtrick::$sudo configd
madtrick::madtrick::$ps aux | grep [c]onfigd

Nota:

Una par de ocasiones en las que realicé estas operaciones sobre configd la terminal se quedo “frita” y el ordenador no queria reiniciarse correctamente.

En otra, system.log indicaba (algo que no se, solo supongo) que se desactivaba la ejecucion del script  enable-network y ssh-config, motivando que cualquier cambio sobre las interfaces de red, no se viera reflejado en la ejecución de estos scripts

Feb 26 13:36:53 madtrick configd[4698]: posting notification com.apple.system.config.network_change
Feb 26 13:36:53 madtrick lookupd[4707]: lookupd (version 369.8) starting - Thu Feb 26 13:36:53 2009
Feb 26 13:36:53 madtrick configd[4698]:   target=enable-network: disabled

Desde mi punto de vista, considero esto normal ya que configd es un componente esencial para el correcto funcionamiento del sistema, por ello cualquier alteración en su funcionamiento (matarlo es un cambio bastante importante, ¿no?) es más que probable que derive en inestabilidad en el sistema.

Llegados a este punto, solo nos queda reiniciar el ordenador (por eso de no jugar mas con configd) y comprobar que nuestro script se ejecuta correctamente.

Por si alguien le interesa el script ssh-config , aqui esta.Lo unico que hace este script es cambiar el puerto para el host git.frusfrusfrus.net de 22 a 2280 o de 2280 a 22 dependiendo de si estoy trabajando desde la red donde se encuentra el repo  o desde casa, respectivamente.

Para terminar, aqui dejo una lista de los enlaces que me fueron utiles.

Networking on OS X

OS X: How to perform an action during fast user switch

1 comment » | programacion

Any fool…

February 22nd, 2009 — 05:09 pm

Any fool can write code that a computer can understand. Good programmers write code that humans can understand

Martin Fowler dixit

Comment » | programacion

Nach oben

« Previous Entries     Next Entries »