Subsecciones


Marshalling

Serializar un objeto es convertirlo en una cadena de bytes con la idea de almacenarlo para su uso posterior o para su uso por otro programa o proceso. Podemos utilizar el módulo Marshal para ello.

Podemos usar marshalling para guardar el estado de un objeto o: en un fichero objmsh

~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n marshal2.rb 
     1  require "Klass"
     2  
     3  o = Klass.new("hello", { :a => 1, :b => 2} ) 
     4  File.open("objmsh", "wb") do |f|
     5    data = Marshal.dump(o, f) 
     6  end
La clase Klass esta definida así:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n Klass.rb 
     1  class Klass 
     2    def initialize(str, hash)
     3      @str, @hash = str, hash 
     4    end 
     5  
     6    def to_s
     7     h = @hash.keys.map { |x| "#{x} => #{@hash[x]}" }.join(" ")
     8     "str = '#@str' hash = {#{h}}"
     9    end 
    10  end
Podemos cargar posteriormente el objeto en la ejecución de un script usando Marshal.load. De este modo es posible guardar el estado de un programa entre ejecuciones:

~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n marshal3.rb 
     1  require "Klass"
     2  
     3  obj = nil
     4  File.open("objmsh","rb") {|f| obj = Marshal.load(f)}
     5  puts obj.inspect
     6  puts obj
La ejecución de marshal2.rb serializa el objeto y lo guarda en un fichero:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby marshal2.rb
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ls -ltr | tail -1
-rw-r--r--  1 casianorodriguezleon  staff    43 13 oct 21:29 objmsh

La ejecución de marshal3.rb reconstruye el objeto usando Marshal.load:

~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby marshal3.rb
#<Klass:0x10016a020 @hash={:a=>1, :b=>2}, @str="hello">
str = 'hello' hash = {a => 1 b => 2}


Marshalling Datos entre Procesos que se Comunican

MacBookdeCasiano:distributedRuby casiano$ cat -n fork.rb
     1  #!/usr/bin/env ruby -w
     2  def do_in_child
     3    read, write = IO.pipe
     4  
     5    pid = fork do
     6      read.close
     7      result = yield
     8      write.puts [Marshal.dump(result)].pack("m")
     9      exit!
    10    end
    11  
    12    write.close
    13    puts "In father. PID = #{Process.pid}"
    14    result = read.read
    15    Process.wait2(pid)
    16    r = Marshal.load(result.unpack("m")[0])
    17    puts "#{Process.pid}: #{r.inspect}"
    18  end
    19  
    20  do_in_child do
    21    puts "In child. PID = #{Process.pid}"
    22    { :a => 1, :b => 2 } 
    23  end

Cuando se ejecuta el programa anterior produce una salida parecida a esta:

MacBookdeCasiano:distributedRuby casiano$ ./fork.rb 
In father. PID = 6583
In child. PID = 6584
6583: {:a=>1, :b=>2}


Ejercicios

  1. ¿Que contiene la variable global $$?
  2. ¿Que contiene la variable global $?? ¿Cual será la salida? (Véase Process::Status).
    >> `ls noexiste`
    ls: noexiste: No such file or directory
    => ""
    >> $?
    => #<Process::Status: pid=1717,exited(1)>
    >> $?.exitstatus
    => 
    >> $?.pid
    => 
    >> $?.signaled?
    => false
    >> $?.success?
    => 
    >> $?.termsig
    => nil
    
  3. ¿Cual será la salida de este programa?
    if (pid = fork) then puts $$ else puts $$ end
    
  4. ¿Cual será la salida de este programa?
         1  a = 4
         2  if (pid = fork)
         3    puts "#{$$}: #{a}"
         4  else 
         5    puts "#{$$}: #{a}"
         6    a  = 5
         7    puts "#{$$}: #{a}"
         8    exit!
         9  end
        10  Process.wait2
        11  puts "#{$$}: #{a}"
    
  5. El programa unix bc implementa una calculadora. He aqui un ejemplo de sesión
    ~/rubytesting/distributedRuby$ bc
    bc 1.06
    Copyright 1991-1994, 1997, 1998, 2000 Free Software Foundation, Inc.
    This is free software with ABSOLUTELY NO WARRANTY.
    For details type `warranty'. 
    a = 2*-4
    a
    -8      
    b = 9+a
    b
    1
    ^D
    ~/rubytesting/distributedRuby$
    
    ¿Cuál es la salida del siguiente programa? Véase la documentación de popen.
    ~/rubytesting/distributedRuby$ cat -n bc.rb 
         1  IO.popen("bc", "r+") do |pipe|
         2    pipe.puts("a = 2*-4")
         3    pipe.puts("a")
         4    output = pipe.gets
         5    puts output
         6  
         7    pipe.puts("b = 9+a")
         8    pipe.puts("b")
         9    pipe.close_write  
        10    output = pipe.gets
        11    puts output
        12  end
    


Marshalling con YAML

La librería YAML nos permite cierta interoperabilidad entre lenguajes.

La interoperabilidad entre lenguajes es la posibilidad de que el código interactúe con código escrito en un lenguaje de programación diferente. La interoperabilidad entre lenguajes puede ayudar a maximizar la reutilización de código y, por tanto, puede mejorar la eficacia del proceso de programación.

Aquí tenemos un ejemplo:

~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n marshallingWithYAML.rb 
     1  require 'YAML'
     2  
     3  fred = {}
     4  fred['name'] = "Fred Astair"
     5  fred['age'] = 54
     6  
     7  laura = {}
     8  laura['name'] = "Laura Higgins"
     9  laura['age'] = 45
    10  
    11  test_data = [ fred, laura ]
    12  
    13  puts "The array dumped by Ruby in YAML format:\n"+YAML::dump(test_data)
    14  
    15  IO.popen('perl perlscript.pl', "w+") do |pipe|
    16    pipe.puts YAML::dump(test_data)
    17    pipe.close_write  
    18    output = pipe.read
    19    puts "The perl script produced this output:\n<<\n#{output}>>"
    20  end
Este es el código del script Perl arrancado desde Ruby:
/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n perlscript.pl 
     1  use strict;
     2  use warnings;
     3  $| = 1;
     4  
     5  use YAML;
     6  
     7  $/ = undef;
     8  my $rubypersons = <STDIN>;
     9  my $perlpersons = Load($rubypersons);
    10  for (@$perlpersons) {
    11    print($_->{name},"\n");
    12  }
Este es el resultado de la ejecución:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby marshallingWithYAML.rb 
The array dumped by Ruby in YAML format:
--- 
- name: Fred Astair
  age: 54
- name: Laura Higgins
  age: 45
The perl script produced this output:
<<
Fred Astair
Laura Higgins
>>

Marshalling con PStore

La librería PStore nos permite almacenar y recuperar estructuras de datos en un fichero.

~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n trivialclass.rb 
     1  class Person
     2    attr_accessor :name, :job, :gender, :age
     3  end

~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n marshalWithPStore.rb
     1  require "trivialclass"
     2  require "pstore"
     3  
     4  fred = Person.new
     5  fred.name = 'Fred Bloggs'
     6  fred.age = 54
     7  
     8  laura = Person.new
     9  laura.name = "Laura Higgins"
    10  laura.age = 45
    11  
    12  store = PStore.new("persistentobjects")
    13  store.transaction do
    14    store[:people] ||= []
    15    store[:people] << fred << laura
    16  end
El método transaction(read_only=false) abre una nueva transacción del almacenamiento de datos. El código que se pasa como bloque puede leer y escribir datos en el fichero. El final del bloque produce un commit automático de los cambios. Es posible producir explícitamente el final de la transacción llamando a PStore::commit:
require "pstore"

store = PStore.new("data_file.pstore")
store.transaction do  # begin transaction
  # load some data into the store...
  store[:one] = 1
  store[:two] = 2

  store.commit        # end transaction here, committing changes

  store[:three] = 3   # this change is never reached
end
también es posible finalizar la transacción llamando a abort:
require "pstore"

store = PStore.new("data_file.pstore")
store.transaction do  # begin transaction
  store[:one] = 1     # this change is not applied, see below...
  store[:two] = 2     # this change is not applied, see below...

  store.abort         # end transaction here, discard all changes

  store[:three] = 3   # this change is never reached
end
Si se genera una excepción dentro del bloque se produce una llamada a abort.

Si el argumento read_only es true, sólo se podrá acceder para lectura al almacen de datos durante la transacción y cualquier intento de cambiarlo producirá una excepción PStore::Error.

Nótese que PStore no soporta transacciones anidadas.

El siguiente script recupera los datos almacenados:

~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ cat -n useMarshalWithPStore.rb
     1  require "trivialclass"
     2  require "pstore"
     3  store = PStore.new("persistentobjects")
     4  people = []
     5  store.transaction do
     6    people = store[:people]
     7  end
     8  people.each { |o|
     9    puts o.inspect
    10  }

Sigue un ejemplo de ejecución:

~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby marshalWithPStore.rb 
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ls -ltr | tail -1
-rw-r--r--  1 casianorodriguezleon  staff    77 16 oct 12:48 persistentobjects
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby useMarshalWithPStore.rb 
#<Person:0x10036ee48 @name="Fred Bloggs", @age=54>
#<Person:0x10036ed80 @name="Laura Higgins", @age=45>

Ejercicio 10.8.1   Si se ejecuta por segunda vez:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby marshalWithPStore.rb 
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ls -ltr | tail -1
-rw-r--r--  1 casianorodriguezleon  staff   125 16 oct 12:49 persistentobjects
Obtenemos:
~/rubytesting/TheRubyProgrammingLanguage/Chapter7ClassesAndModules$ ruby useMarshalWithPStore.rb 
#<Person:0x10036ee48 @name="Fred Bloggs", @age=54>
#<Person:0x10036ed80 @name="Laura Higgins", @age=45>
#<Person:0x10036ed08 @name="Fred Bloggs", @age=54>
#<Person:0x10036ec90 @name="Laura Higgins", @age=45>
¿Cual es la razón?


Marshalling Lambdas y Procs

Los métodos dump y load no funcionan con objetos de la clase Proc. Cuando ejecutamos este programa:
~/rubytesting/distributedRuby/serializeProc$ cat -n marshal2.rb 
     1  o = Proc.new { |a,b | a+b }  
     2  File.open("objmsh", "wb") do |f|
     3    data = Marshal.dump(o, f) 
     4  end
Obtenemos un mensaje de error:

~/rubytesting/distributedRuby/serializeProc$ ruby marshal2.rb 
marshal2.rb:3:in `dump': no marshal_dump is defined for class Proc (TypeError)
  from marshal2.rb:3
  from marshal2.rb:2:in `open'
  from marshal2.rb:2

Algunos objetos no pueden ser volcados. Si nuestra clase tiene necesidades especiales debemos implementar nuestra propia estrategia de serialización definiendo dos métodos:

  1. _dump

    El método de instancia _dump debe retornar una cadena String que contenga la información necesaria para la reconstrucción del objeto hasta la profundidad indicada por un parámetro entero. Un valor de -1 de dicho parámetro indica que desactivamos la comprobación de profundidad.

  2. _load

    El método de clase _load toma una String y devuelve el objeto reconstruido.

Veamos un ejemplo de uso de _dump y _load. La clase SerializableProc permite serializar Procs:

~/rubytesting/distributedRuby/serializeProc$ cat -n SerializableProc.rb 
     1  class SerializableProc
     2  
     3     def initialize( block )
     4       @block = block
     5       @block.sub!(/^/,'lambda ') unless @block =~/^\s*(?:lambda|proc)/
     6       # Test if block is valid.
     7       @func = eval "#{@block}"
     8     end
     9  
    10  
    11     def call(* args)
    12       @func.call(* args)
    13     end
    14  
    15     def arity
    16       @func.arity
    17     end
    18  
    19     def _dump(limit)
    20       @block
    21     end
    22  
    23     def self._load(s)
    24       self.new(s)
    25     end
    26  
    27  end
Este código hace uso de la clase:
~/rubytesting/distributedRuby/serializeProc$ cat -n marshalproc2.rb 
     1  
     2  require "SerializableProc"
     3  
     4  
     5  if $0 == __FILE__
     6  
     7     code = SerializableProc.new %q{ { |a,b| a+b }}
     8  
     9     # Marshal
    10     File.open('proc.marshalled', 'w') { |file| Marshal.dump(code, file) }
    11     code = File.open('proc.marshalled') { |file| Marshal.load(file) }
    12  
    13     p code.call( 1, 2 )
    14  
    15     p code.arity
    16  
    17  end
Sigue una ejecución:
~/rubytesting/distributedRuby/serializeProc$ ruby marshalproc2.rb 
3
2

Este otro código muestra otra solución que no hace uso de _dump y _load:

~/rubytesting/distributedRuby/serializeProc$ cat -n serializableProc2.rb 
     1  class SerializableProc
     2  
     3     @@cache = {}
     4     def initialize( block )
     5       @block = block
     6       @block.sub!(/^/,'lambda ') unless @block =~/^\s*(?:lambda|proc)/
     7       # Test if block is valid.
     8       @@cache[@block] = eval "#{@block}"
     9     end
    10  
    11  
    12     def call(* args)
    13       @@cache[@block].call(* args)
    14     end
    15  
    16     def arity
    17       @@cache[@block].arity
    18     end
    19  
    20  end
    21  
    22  
    23  if $0 == __FILE__
    24  
    25     require 'yaml'
    26     require 'pstore'
    27  
    28     code = SerializableProc.new %q{ { |a,b| a+b }}
    29  
    30     # Marshal
    31     File.open('proc.marshalled', 'w') { |file| Marshal.dump(code, file) }
    32     code = File.open('proc.marshalled') { |file| Marshal.load(file) }
    33  
    34     p code.call( 1, 2 )
    35  
    36     # PStore
    37     store = PStore.new('proc.pstore')
    38     store.transaction do
    39       store['proc'] = code
    40     end
    41     store.transaction do
    42       code = store['proc']
    43     end
    44  
    45     p code.call( 1, 2 )
    46  
    47     # YAML
    48     File.open('proc.yaml', 'w') { |file| YAML.dump(code, file) }
    49     code = File.open('proc.yaml') { |file| YAML.load(file) }
    50  
    51     p code.call( 1, 2 )
    52  
    53     p code.arity
    54  
    55  end
La solución "memoiza" en un hash que es un atributo de la clase la relación entre el fuente y el código. Cuando se ejecuta el código anterior se obtiene:
~/rubytesting/distributedRuby/serializeProc$ ruby serializableProc2.rb 
3
3
3
2


Práctica: Procesos Concurrentes

Generalize el ejemplo visto en la sección [*] escribiendo un método prun que permita lanzar un número dado de procesos:

res = prun(
     :one     => lambda { |n| "#{n} is 1" },
     :three   => lambda { |n| "#{n} is 3" },
     'two'    => lambda { |n| n*4 }
)

puts res.inspect

res = prun(
     [2, 3, 7] => lambda { |n| n[0]+n[1]+n[2] },
     [4, 5]    => lambda { |n| n[0]*n[1] }
)

puts res.inspect
Este programa debería producir una salida similar a esta:
~/rubytesting/distributedRuby$ ruby parfork2.rb 
{"two"=>"twotwotwotwo", :one=>"one is 1", :three=>"three is 3"}
{[2, 3, 7]=>12, [4, 5]=>20}

Casiano Rodriguez León 2015-01-07