Mariano Guerra: Making a Chat App with Erlang, Rebar, Cowboy and Bullet

   Publicado:

this is a continuation of the post about erlang, cowboy and rebar.

let' start by adding the bullet dep:

{sub_dirs, ["rel"]}.

{deps, [
    {cowboy, "1.0.0", {git, "https://github.com/ninenines/cowboy", {tag, "1.0.0"}}},
    {bullet, "0.4.1", {git, "https://github.com/extend/bullet", {tag, "0.4.1"}}}
]}.

now get the deps:

./rebar get-deps

add bullet as a dependency on disrupt.app.src:

{application, disrupt,
 [
  {description, ""},
  {vsn, "1"},
  {registered, []},
  {applications, [
                  kernel,
                  stdlib,
                  cowboy,
                  bullet
                 ]},
  {mod, { disrupt_app, []}},
  {env, []}
 ]}.

register our bullet handler with cowboy:

start(_StartType, _StartArgs) ->
    {ok, ChannelPid} = disrupt_channel:new(),
    Dispatch = cowboy_router:compile([
        {'_', [
               {"/chat", bullet_handler, [{handler, disrupt_chat_handler}, {channel, ChannelPid}]},
               {"/ui/[...]", cowboy_static, {priv_dir, disrupt, "assets",
                                             [{mimetypes, cow_mimetypes, all}]}}
        ]}
    ]),
    {ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
        {env, [{dispatch, Dispatch}]}
    ]),
    disrupt_sup:start_link().

the important lines are:

{ok, ChannelPid} = disrupt_channel:new(),

were we create a new chat channel that will be passed to all disrupt_chat_handler instances here:

{"/chat", bullet_handler, [{handler, disrupt_chat_handler}, {channel, ChannelPid}]},

also note that I changed the path of the ui to /ui/... instead of it being at the root.

add the js libs we need to use bullet (yes, I could use bower, but let's keep it simple):

mkdir priv/assets/vendor
mkdir priv/assets/js
cp deps/bullet/priv/bullet.js priv/assets/vendor
wget http://code.jquery.com/jquery-2.1.1.min.js -O priv/assets/vendor/jquery.js

we create a really simple chat page at index.html:

<!doctype html>
<html>
 <head>
  <title>Chat!</title>
  <meta charset="utf-8">
  <script src="vendor/jquery.js"></script>
  <script src="vendor/bullet.js"></script>
  <script src="js/app.js"></script>
  <style>
   body{ font-family: helvetica; color: #333; background-color: #fefefe;
    margin-left: 25%; width: 50%; }
   p, textarea{ padding: 0; }
   p, button, textarea{ width: 100%; margin: 1em 0; }
   label{ float: left; width: 45%; }
   input{ width: 50%; float: right; }
   input, textarea{ border: 1px solid #ddd; }
  </style>
 </head>
 <body>
  <p>
  <label for="input">Nickname</label>
  <input type="text" id="nick" value="anonymous"/>
  </p>
  <textarea id="output" cols="80" rows="25"></textarea>
  <p>
  <label for="input">Input</label>
  <input type="text" id="input"/>
  <button id="send">Send</button>
  </p>
 </body>
</html>

add code to handle the chat app on priv/assets/js/app.js:

/*globals $, document, window*/
function disruptApp(document, window, $) {
    'use strict';
    var input = document.getElementById('input'),
        output = document.getElementById('output'),
        nickInput = document.getElementById('nick'),
        send = document.getElementById('send'),

        connection;

    function sendMessage(text) {
        var nick = getNick();
        connection.send(nick + ': ' + text);
    }

    function onSendClicked() {
        var text = input.value.trim();

        if (text !== '') {
            sendMessage(text);
        }

        input.value = '';
    }

    function getNick() {
        var nick = nickInput.value.trim();

        if (nick === '') {
            return 'anonymous';
        } else {
            return nick;
        }
    }

    function notify(text) {
        var date = (new Date()).toLocaleString();
        output.innerHTML = output.innerHTML + '[' + date + '] ' + text + '\n';
    }

    function onData(data) {
        notify(data);
    }

    send.addEventListener('click', onSendClicked);

    function start(url, options, notify, onData) {
        var connection = $.bullet(url, options);

        connection.onopen = function(){
            notify('online');
        };

        connection.onclose = connection.ondisconnect = function(){
            notify('offline');
        };

        connection.onmessage = function(e){
            if (e.data === 'pong'){
                notify('pong');
            } else {
                onData(e.data);
            }
        };

        connection.onheartbeat = function(){
            connection.send('ping');
            notify('ping');
        };

        return connection;
    }

    connection = start('ws://localhost:8080/chat', {}, notify, onData);
}

document.addEventListener("DOMContentLoaded", function() {
    'use strict';
    disruptApp(document, window, $);
});

we need a simple pubsub mechanism for channels, I won't explain it here in detail but you can read about erlang's gen_event behaviour which is the one that does all the work:

-module(disrupt_channel).
-behaviour(gen_event).

-export([new/0, subscribe/2, unsubscribe/2, send/2]).

-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3,
         terminate/2]).
%% API

new() -> gen_event:start_link().

subscribe(Channel, Pid) ->
    gen_event:add_handler(Channel, {disrupt_channel, Pid}, [Pid]).

unsubscribe(Channel, Pid) ->
    gen_event:delete_handler(Channel, {disrupt_channel, Pid}, [Pid]).

send(Channel, Event) ->
    gen_event:notify(Channel, Event).

-record(state, {pid}).
%% callbacks
init([Pid]) -> {ok, #state{pid=Pid}}.

handle_event(Msg, State=#state{pid=Pid}) ->
    Pid ! Msg,
    {ok, State}.

handle_call(_, State) -> {ok, ok, State}.

handle_info(_, State) -> {ok, State}.

code_change(_OldVsn, State, _Extra) -> {ok, State}.

terminate(_Reason, _State) -> ok.

the important part from the code above is the fact that we store the pid of the bullet handler on the gen_event instance so we can send the message back when we get notified:

handle_event(Msg, State=#state{pid=Pid}) ->
    Pid ! Msg,
    {ok, State}.

the bullet handler for the chat channel is:

-module(disrupt_chat_handler).

-export([init/4, stream/3, info/3, terminate/2]).

-record(state, {channel}).

init(_Transport, Req, Opts, _Active) ->
    io:format("channel init ~p~n", [Opts]),
    {channel, ChannelPid} = lists:keyfind(channel, 1, Opts),
    disrupt_channel:subscribe(ChannelPid, self()),
    {ok, Req, #state{channel=ChannelPid}}.

stream(<<"ping">>, Req, State) ->
    io:format("ping received~n"),
    {reply, <<"pong">>, Req, State};

stream(Data, Req, State=#state{channel=ChannelPid}) ->
    io:format("message received ~s~n", [Data]),
    disrupt_channel:send(ChannelPid, {msg, self(), Data}),
    {ok, Req, State}.

info({msg, _Sender, Data}, Req, State) ->
    io:format("msg received ~p~n", [Data]),
    {reply, Data, Req, State}.

terminate(_Req, #state{channel=ChannelPid}) ->
    io:format("unsubscribing from channel~n"),
    disrupt_channel:unsubscribe(ChannelPid, self()),
    ok.

we subscribe on init, unsubscribe on terminate, when we receive a ping message we reply it only to the sender with pong, if we receive something else we send the message to the channel so it gets sent to all subscribers, that get the message on the info function where they send it to the browsers.

build:

rm -rf rel/disrupt && ./rebar compile generate

run:

./rel/disrupt/bin/disrupt console

open http://localhost:8080/ui/index.html in two or more browsers and chat!

../galleries/misc/bullet-chat.png

Mariano Guerra: Serving Static Files with Erlang, Cowboy and Rebar, (raw material)

   Publicado:

the "serving static files from a server" market needs some disruption, let's tackle that problem with some erlang.

create the folder:

mkdir disrupt
cd disrupt

get rebar:

wget http://cloud.github.com/downloads/basho/rebar/rebar
chmod u+x rebar

generate app:

./rebar create-app appid=disrupt

add dependencies:

vim rebar.config

the file should contain the following code:

{deps, [
    {cowboy, "1.0.0", {git, "https://github.com/ninenines/cowboy", {tag, "1.0.0"}}}
]}.

this tells to get cowboy 1.0.0 from git as dependency, let's fetch the dependencies:

./rebar get-deps

now let's serve some static files, open src/disrupt_app.erl, change the start function so it looks like this:

start(_StartType, _StartArgs) ->
    Dispatch = cowboy_router:compile([
        {'_', [
            {"/[...]", cowboy_static, {priv_dir, disrupt, "assets",
                [{mimetypes, cow_mimetypes, all}]}}
        ]}
    ]),
    {ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
        {env, [{dispatch, Dispatch}]}
    ]),
    disrupt_sup:start_link().

the code above is taken from the static example from cowboy adapted to our needs: https://github.com/ninenines/cowboy/tree/master/examples/static_world

docs here: http://ninenines.eu/docs/en/cowboy/1.0/guide/static_handlers/

now make the priv/assets folder:

mkdir -p priv/assets

and put some content in the index file:

echo "hello static world" > priv/assets/index.html

let's make a release for the project, first create the release files:

mkdir rel
cd rel
../rebar create-node nodeid=disrupt
cd ..

edit rel/reltool.config, the line:

{lib_dirs, []},

should change to:

{lib_dirs, ["../deps"]},

add the line:

sub_dirs, ["rel"]}.

to the top of the rebar.config file

if not you get this error:

Command 'generate' not understood or not applicable

yay helpfulness!

now we are one confusing error closer to our goal, but now we get:

ERROR: generate failed while processing /home/mariano/tmp/disrupt/rel: {'EXIT',{{badmatch,{error,"disrupt: Missing application directory."}},
         [{rebar_reltool,generate,2,[]},
          {rebar_core,run_modules,4,[]},
          {rebar_core,execute,5,[]},
          {rebar_core,process_dir0,6,[]},
          {rebar_core,process_dir,4,[]},
          {rebar_core,process_each,5,[]},
          {rebar_core,process_dir0,6,[]},
          {rebar_core,process_dir,4,[]}]}}

only because I've seen some other erlang projects I decided that this may be the solution:

mkdir -p apps/disrupt
cd apps/disrupt
ln -s ../../src
ln -s ../../ebin
ln -s ../../priv
cd ../..

we try again to make the release and we get the error again, nice!

for no apparent reason that the fact that I saw it on some other project, change the line on rel/reltool.config:

{lib_dirs, ["../deps"]},

to:

{lib_dirs, ["../deps", "../apps"]},

trying again:

./rebar compile generate

success!

let's try running it:

./rel/disrupt/bin/disrupt console

and what do we get? well, a crash! \o/:

Exec: /home/mariano/tmp/disrupt/rel/disrupt/erts-5.10.4/bin/erlexec -boot /home/mariano/tmp/disrupt/rel/disrupt/releases/1/disrupt -mode embedded -config /home/mariano/tmp/disrupt/rel/disrupt/releases/1/sys.config -args_file /home/mariano/tmp/disrupt/rel/disrupt/releases/1/vm.args -- console
Root: /home/mariano/tmp/disrupt/rel/disrupt
Erlang R16B03 (erts-5.10.4) [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V5.10.4  (abort with ^G)
(disrupt@127.0.0.1)1>
=INFO REPORT==== 29-Aug-2014::11:49:32 ===
    application: disrupt
    exited: {bad_return,
             {{disrupt_app,start,[normal,[]]},
              {'EXIT',
               {undef,
                [{cowboy_router,compile,
                  [[{'_',
                     [{"/[...]",cowboy_static,
                       {priv_dir,disrupt,"assets",
                        [{mimetypes,cow_mimetypes,all}]}}]}]],
                  []},
                 {disrupt_app,start,2,
                  [{file,"src/disrupt_app.erl"},{line,13}]},
                 {application_master,start_it_old,4,
                  [{file,"application_master.erl"},{line,269}]}]}}}}
    type: permanent
{"Kernel pid terminated",application_controller,"{application_start_failure,disrupt,{bad_return,{{disrupt_app,start,[normal,[]]},{'EXIT',{undef,[{cowboy_router,compile,[[{'_',[{\"/[...]\",cowboy_static,{priv_dir,disrupt,\"assets\",[{mimetypes,cow_mimetypes,all}]}}]}]],[]},{disrupt_app,start,2,[{file,\"src/disrupt_app.erl\"},{line,13}]},{application_master,start_it_old,4,[{file,\"application_master.erl\"},{line,269}]}]}}}}}"}

Crash dump was written to: erl_crash.dump
Kernel pid terminated (application_controller) ({application_start_failure,disrupt,{bad_return,{{disrupt_app,start,[normal,[]]},{'EXIT',{undef,[{cowboy_router,compile,[[{'_',[{"/[...]",cowboy_static

with all my time reading erlang crashes I see an undef there, it seems the app cant find cowboy_router:compile

but I have it on my deps..

well, let's put it on src/disrupt.app.src applications for no other reason that I've seen people do that, the file should look like this:

{application, disrupt,
 [
  {description, ""},
  {vsn, "1"},
  {registered, []},
  {applications, [
                  kernel,
                  stdlib,
                  cowboy
                 ]},
  {mod, { disrupt_app, []}},
  {env, []}
 ]}.

see the cowboy as last element on the applications list? that's what you should add.

let's try again:

./rebar compile generate

and I get:

<some output removed here>

==> rel (generate)
ERROR: generate failed while processing /home/mariano/tmp/disrupt/rel: {'EXIT',{{badmatch,{error,"Application cowboy is used in release \"disrupt\" and cannot be excluded"}},
         [{rebar_reltool,generate,2,[]},
          {rebar_core,run_modules,4,[]},
          {rebar_core,execute,5,[]},
          {rebar_core,process_dir0,6,[]},
          {rebar_core,process_dir,4,[]},
          {rebar_core,process_each,5,[]},
          {rebar_core,process_dir0,6,[]},
          {rebar_core,process_dir,4,[]}]}}

"Application cowboy is used in release "disrupt" and cannot be excluded"

who told you to exclude it?

let's apply a technic we already used a lot before, let's make up reasons for what it may be failing, it says exclude there and I've seen a lot of exclude and include atoms in rel/reltool.config, maybe it's that?

let's layer some other technic I use a lot, let's try to make that file look as similar as another one I've seen that works, in this case it's the reltool.config file generated by riak_core rebar template:

https://github.com/basho/rebar_riak_core/blob/master/riak_core.reltool.config

ok, that one seems to have less stuff than ours, let's start commenting everything that looks different until we reach this point:

{sys, [
    {lib_dirs, ["../deps", "../apps"]},
        %{erts, [{mod_cond, derived}, {app_file, strip}]},
        %{app_file, strip},
        {rel, "disrupt", "1",
            [
                kernel,
            stdlib,
            sasl,
            disrupt
                ]},
        {rel, "start_clean", "",
            [
                kernel,
            stdlib
                ]},
        {boot_rel, "disrupt"},
        {profile, embedded},
        %{incl_cond, exclude},
        {excl_archive_filters, [".*"]}, %% Do not archive built libs
    {excl_sys_filters, ["^bin/.*", "^erts.*/bin/(dialyzer|typer)",
        "^erts.*/(doc|info|include|lib|man|src)"]},
        {excl_app_filters, ["\.gitignore"]},
        {app, sasl,   [{incl_cond, include}]},
        %{app, stdlib, [{incl_cond, include}]},
        %{app, kernel, [{incl_cond, include}]},
        {app, disrupt, [{incl_cond, include}]}
    ]}.

{target_dir, "disrupt"}.

{overlay, [
    {mkdir, "log/sasl"},
        {copy, "files/erl", "\{\{erts_vsn\}\}/bin/erl"},
        {copy, "files/nodetool", "\{\{erts_vsn\}\}/bin/nodetool"},
        {copy, "files/disrupt", "bin/disrupt"},
        {copy, "files/disrupt.cmd", "bin/disrupt.cmd"},
        {copy, "files/start_erl.cmd", "bin/start_erl.cmd"},
        {copy, "files/install_upgrade.escript", "bin/install_upgrade.escript"},
        {copy, "files/sys.config", "releases/\{\{rel_vsn\}\}/sys.config"},
        {copy, "files/vm.args", "releases/\{\{rel_vsn\}\}/vm.args"}
    ]}.

trying again:

rm -rf rel/disrupt
./rebar compile generate

success!

now let's try running it:

./rel/disrupt/bin/disrupt console

the output we get:

Exec: /home/mariano/tmp/disrupt/rel/disrupt/erts-5.10.4/bin/erlexec -boot /home/mariano/tmp/disrupt/rel/disrupt/releases/1/disrupt -mode embedded -config /home/mariano/tmp/disrupt/rel/disrupt/releases/1/sys.config -args_file /home/mariano/tmp/disrupt/rel/disrupt/releases/1/vm.args -- console
Root: /home/mariano/tmp/disrupt/rel/disrupt
Erlang R16B03 (erts-5.10.4) [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V5.10.4  (abort with ^G)
(disrupt@127.0.0.1)1>

that's what I would call a non crashing system, let's see if it works:

curl http://localhost:8080/index.html

we get:

hello static world

success!

and that's how you serve a static file with erlang, cowboy and rebar :)

Gabriel Patiño: OpenStreetMap en tu GPS Garmin

   Publicado: A principios del 2008 me crucé con un proyecto que me había parecido muy interesante: OpenStreetMap. Es un sitio donde podés ayudar a definir mapas del mundo real, no solo calles, sino también puntos de interés, escuelas, recorridos de colectivos, paradas... prácticamente un relevamiento total del planeta.

Funciona al estilo Wikipedia: el que sabe de alguna región, colabora. También hay mucha gente que releva caminos o puntos con un GPS, y luego se basa en la información obtenida. Otros prefieren una opción un poco más simple pero no tan precisa, y es mapear sobre los mapas satelitales de acceso público.

Desde que lo descubrí, fui colaborando tímidamente por las zonas que conocía, arreglando alguna calle con el sentido mal definido, agregando algo nuevo que veía en las vacaciones, etc. Cuando me mudé a San Martín de los Andes a principios del 2013 vi que por acá faltaban relevar muchas calles y barrios, principalmente los que no son de fácil acceso, así que aprovechaba cada salida en auto o caminando y me llevaba el GPS para registrar los recorridos.

Actualmente el mapa está lo suficientemente detallado como para preguntarme: ¿se podrá poner este mapa en el GPS del auto?

Tengo un Garmin, y hasta ahora solo tenía disponible el mapa del proyecto Mapear. No es que no me guste ni tengo nada en contra de este proyecto, es más, creo que el 99% de los usuarios de GPS lo deben tener y no saben que existe otra opción. El tema es como se maneja cada proyecto. El proyecto Mapear no es un proyecto abierto, podés colaborar pero siempre a través de la barrera que te pueden poner los propietarios del mismo, o sea, si por algún motivo no quieren aceptar tu colaboración, alpiste. Otro tema es que los datos del mapa no son de dominio público, o sea que no podrías hacer cualquier cosa con el mapa. En pocas palabras, te permiten usarlo pero no modificarlo.

En cambio, el proyecto OpenStreetMap (OSM) te permite agregar directamente lo que quieras o necesites. Podes usar los mapas para cualquier uso, ponerlos en tu sitio, y en algunos lugares (como SMAndes) están mucho más detallados que los mapas de Google Maps. Todo gracias a la gente que colabora, sean vecinos o visitantes que colaboraron con lo que vieron en las vacaciones.

Volviendo al tema y como era de esperar, no fui el primero con la idea de usar OSM en el Garmin. Con poco buscar encontré varias opciones, algunas tan interesantes como un sitio que te genera los mapas para la parte del mundo que quieras, espectacular para viajeros.

La opción que más me gustó es la que armó un colaborar del equipo de OSM de Argentina, Martin Andres Gomez Gimenez, pero sólo es para Argentina y países limítrofes.

Se vé particularmente bien en el GPS, y genera una nueva versión aproximadamente una vez por mes. Está buenísimo ver errores o cosas que faltan cuando vas circulando, llegás a casa y los arreglas en OSM, esperás una nueva versión y listo, problema resuelto.

La instalación es sumamente simple:
  • Se bajan el archivo gmapsupp.img de alguno de estos enlaces:
  • Copian el archivo al GPS, para esto hay dos opciones:
  • Via USB:
    • Conectas el GPS a la compu por USB.
    • Copias el archivo en la carpeta del GPS donde ya exista un archivo con el mismo nombre.
  • Via tarjeta de memoria (para los GPS con tarjeta de memoria):
    • Pones la tarjeta en la compu.
    • Copias el archivo en la carpeta donde ya exista un archivo con el mismo nombre.
  • En ambos casos es recomendable guardar el archivo gmapsupp.img original por las dudas.
  • Tengo entendido que en algunos GPS podés tener varios mapas, en ese caso se puede renombrar el archivo lo por lo que quieras manteniendo la extensión img. No lo pude probar porque mi GPS no lo soporta.

Bueno, eso es todo, que lo disfruten, y a colaborar con el proyecto OSM que esta muy bueno y es muy fácil.

Juan Bautista Cabral: Periodicidad y distribución de dos enfermedades del maíz (Zea mays L) en la decada del 2000 en Argentina (Biomat 2014)

   Publicado:

Poster presentado en biomat 2014


Facundo Batista: Jardinera

   Publicado:


Sí, ya sé que estoy denso con las fotitos de Malena... ¿¿¿pero qué quieren que haga???

<baba>
Malena jardinera
</baba>

Facundo Batista: ¡Carne!

   Publicado:


Mi familia es bastante carnívora (como sucede con los argentinos en general). Cuando vivía con mis padres, no tenía mayor injerencia en cómo, dónde o cuando se compraba la carne (aunque existe la anécdota de que yo, de niño, siguiendo una receta, fuí a la carnicería del barrio a comprar "un kilo de ternera", y el carnicero me preguntaba "¿pero qué corte"?).

Cuando volé del nido, una de las cuestiones que tuve que decidir y hacerme cargo, fue, obviamente ¿dónde comprar la carne?

No es una pregunta sencilla. Bah, la pregunta es sencilla, lo que no es simple es la respuesta. Para simplificar, voy a contar solamente mi última experiencia.

A muchos les gusta construir una "relación especial" con el carnicero/a del barrio (no encontré una manera de escribir eso que no suene un poco pornográfico). De esa manera, siempre tratan de conseguir un corte lindo, una mejor atención, etc. Mi problema es que en la zona donde vivo la carne está cara. Eso me llevó a ir buscando precios por un lado, por el otro, y al final terminé en Chalín, un frigorífico que descubrió mi viejo por la zona de Mataderos.

Mataderos es lejos de casa, sin embargo. Durante mucho tiempo Chalín hacía delivery, con lo cual me traían el pedido a casa (entonces, me resbalaba que fuese lejos).  Desde hace un tiempo, sin embargo, no tienen más ese servicio, pero se puede encargar el pedido y pasarlo a buscar. Aunque no podés elegir el pedazo de carne puntualmente (esa tira de asado, ese pedacito de vacío), pasar a buscar el pedido tiene la ventaja de que no tenés que hacer la cola.

¡Y no hacer la cola es importante! Es que en Chalín se junta gente. Normalmente, tenés una espera de dos o tres horas. Pero me ha pasado de ir en una fecha complicada (para las fiestas de fin de año), llegar a las 7:50 de la mañana, y encontrarme con que tenía 270 personas adelante!! Me terminé yendo a las 12:40 :/

Ahora, ¿tiene sentido ir a buscar la carne hasta allá? De Olivos a Mataderos hay una distancia respetable, y aunque en tiempo no es tanto, hay un gasto de nafta, etc. Una vez, a principio de año, comparé los precios y calculé que, para la compra que había hecho, en Chalín gasté $998, y en la carnicería del barrio habría gastado $1309. Sí, más de 300 mangos de diferencia.

En fin. El punto es que hace mucho tiempo que compro en Chalín, para el consumo de la casa, para cuando hago asados por un cumpleaños, o mis asados geek. Incluso tengo el historial de precios, desde hace cuatro años y medio, que les comparto acá.

Carne

No dejo de buscar alternativas, sin embargo, por si deja de ser la mejor opción. El otro día un conocido que respeto mucho culinariamente me recomendó el Frigorífico Las Heras, que tienen delivery, y decidí probarlos.

Tienen varias cajas, paquetes prearmados de opciones. Por ejemplo, yo compré la "Familiar", que traía "cortes de milanesa, picada, y peceto" (según me dijeron por teléfono). Cuando llegó la caja, ví que tenía 3.5kg de picada, los cortes para milanesa eran 1.5kg de nalga, 1.6kg de cuaddrada, y 1.3kg de bola de lomo, y finalmente el peceto, 1.4kg.

Las principales ventajas de este frigorífico son que te traen el pedido a tu casa, y que viene empaquetado al vacío, super cómodo de manejar y meter en el freezer.

Pero aunque la carne es un poco mejor que en Chalín, me cobraron $70 el kilo (mientras que en Chalín, a esas cantidades de esos cortes, hubiese tenido un promedio de $51 por kg). Y no hay una diferencia de calidad que amerite esa diferencia de precios.

Sí, a Chalín la tengo que ir a buscar. ¡Pero compro los cortes que quiero! Finalmente, un detalle no menor: en Chalín *siempre* me dieron un ticket AFIP válido, y del frigorífico Las Heras no me trajeron boleta.

Juanjo Conti: Cómo un programador escribe una novela

   Publicado:

La semana pasada Colectivo Libre organizó una conferencia (llamada Conferencia de Software Libre del Litoral) y me invitaron a hablar sobre cultura libre o crowdfunding o Creative Commons. En particular, que cuente mi experiencia.

Di una charla llamada Cómo un programador escribe una novela: crowdfunding, herramientas y licencias en la que conté mis peripecias financiando la impresión de mis libros (como ese camino fue evolucionando), hablé de las herramientas que desarrollé para imprimir Xolopes y cerré explicando brevemente las distintas licencias Creative Commons disponibles.

La presentación la armé con slides.com, un sitio que utiliza la librería reveal.jshttp://slides.com/juanjoconti/como-un-programador-escribe-una-novela#/





Cómo un programador escribe una novela

(más fotos)

Luego de la presentación hubieron muchos comentarios y preguntas. Estuvo muy bueno. ¿Te al perdiste? Probablemente vuelta a darla en la Feria del Libro de Santa Fe el sábado 13 de septiembre a las 18 hs.

Lamentablemente no fue grabada y las slides dicen poco por si solas. Sin embargo, acabo de recuperar el borrador de un post que nunca publiqué. Su contenido coincide con un tercio de la charla, aunque tiene mucho más detalle técnico del que expuse en la conferencia:

Cómo escribí Xolopes

Este artículo habla sobre la novela corta Xolopes.

Durante mis últimas vacaciones estuve tomando notas rápidas en el celular. Ideas, conversaciones, principios, finales. Al mismo tiempo estuve leyendo una novela cuya estructura me gustó mucho y quise imitar. Quise hacer un cover.

—¿Y qué escribís? —se interesa el marido de la señora.
—Cuentos.
—Ah… hay que tener imaginación para eso.
—No tanta, me la paso recogiendo voces de otros.

Xolopes #46

La novela sería entonces una sucesión de textos, en su mayoría cortos, con un orden propio. El problema era que no conocía ese orden, que nuevas partes irían surgiendo y que otras serían eliminadas. Escribir todo en un único documento, “un Word”, no parecía una elección correcta. Como me gusta programar, la solución a mi problema sería un programa.

… en lugar de ser una sucesión de pocos capítulos largos, se forma con una multiplicidad de textos cortos, uno a continuación del otro, una multitud de voces que van ingresando al texto para formarlo.

Xolopes #122

Pasé cada una de las partes que tenía hasta el momento a archivos de texto. Uno por cada parte. En otro archivo (índice) escribí un orden tentativo para las partes. Finalmente, escribí un script en Python (que se fue refinando y extendiendo) capás de leer índice y armar un documento Latex con las partes y el orden especificado. Ese documento Latex puede compilarse a pdf, generando el archivo que voy a enviar a la imprenta.

Con el correr de los meses fui escribiendo el resto de las partes. Algunas historias son recuerdos de otros viajes, otras son el resultado de ejercicios en el taller, otras, anécdotas prestadas y otras, totales inventos. Mientras lo hacía, iba probando distintos ordenes hasta encontrar EL ORDEN definitivo que tendría la obra, ese que tenía desde antes de ser escrita pero que aún no conocía.

Control de cambios

Me resulta entretenido participar de todo el proceso de la concepción de un libro. Desde tipear sus palabras hasta imprimirlo y voy refinando e incrementando mi participación en todo el proceso lo más que puedo. En este experimento, a diferencia de los dos anteriores (1 y 2), intento

Además de escribir la novela usando un programa escrito especialmente para escribirla, la misma fue escrita de forma similar a la que se escribe un programa. Utilicé un sistema de control de versiones para llevar registro de cada uno de los cambios que iba haciendo.

Ejemplo: https://github.com/jjconti/novela-workflow/commit/47805ecaf6d873363f10220b27113ef53b6f1855#diff-b1d745729a42a8029705f6c847066918

Herramientas

Otras herramientas fueron escritas a la vez que se escribía la novela pero no quiero irme en detalles. Temo aburrir con muchos tecnicismos a los lectores, autores, editores que lean este post. También temo aburrir con muchas palabras a los hackers, programadores y geeks que lean este post. Por lo tanto voy a cerrarlo ahora mismo :)

Este es el set de herramientas propias y archivos utilizado para generar el pdf de la novela al momento de escribir este post. A los ojos de un programador, son herramientas simples que uno puede hacerse en un su casa. Pero tal vez les sean interesantes a autores o editores:

  • format.sh (evita tener archivos muy “anchos” limitando el número de columnas)
  • xolopesBase.tex (template del documento Latex utilizado)
  • regenerateIndex.py (le agrega al índice las primeras palabras de cada archivo para tener contexto y ayudarme a no perderme entre los nombres de archivos)
  • xolopes.sh (programa maestro, llama a xolopes.py y compila los documentos Latex a pdf)
  • xolopes.py (genera el documento Latex en base a las partes y al índice, en dos versiones, con imágenes y sin imágenes)
  • getDot.py (genera el archivo .dot que representa el mapa de la novela)
  • map.sh (en base al arcihvo .dot que representa el mapa de la novela genera el mapa en distintos formatos)

Gabriel Patiño: Mazo de leña

   Publicado:

Hace rato que andaba necesitando un mazo para darle a los formones o encajar los encastres de madera cuando hago algún proyecto, así que me hice uno, y de la forma más simple y barata:

Fui hasta la pila de leña, busque un pedazo para la cabeza, y otro para el mango, cortar, encastrar, y lijar un poquito.


Listo, un mazo que no marca la madera por $0. La madera es álamo de unos troncos que cortaron unos chicos en el fondo de casa, y estaba esperando a ser consumida en el próximo asado.


Este mazo fué indispensable para hacer las colas de milano del porta rollos que hice hace poco, y creo que va a seguir funcionando por un buen rato.

Lo bueno es que si se rompe o no me sirve, igualmente puedo usarlo en alguna parrillada :)

Algunos detalles:


Juanjo Conti: Aprendiendo Ruby: desde Python

   Publicado:

Hace un mes y medio estoy estudiando y trabajando con el lenguaje de programación Ruby. Después de unos cuatro años con Python y luego más de tres con PHP/JS, el cambio se sintió como una ráfaga de aire fresco y en esa ráfaga reconocí aromas del pasado.

El primer día que use Ruby, leí este artículo:

https://www.ruby-lang.org/en/documentation/ruby-from-other-languages/to-ruby-from-python/

Es bastante incompleto.

Ruby es muy parecido a Python. Pero hay muchos detalles que te pueden morder. Mientras iba estudiando, fui haciendo una lista de los que me mordían. Esa lista se convirtió en mi primer charla de Ruby.

El jueves 7 de agostó se llevó a cabo la tercera meetup del año de RubyLit (el grupo de usuarios de Ruby local). En esta presenté una charla titulada Aprendiendo Ruby: desde Python. Las slides de la presentación fueron generadas en base a un notebook IPython usando el kernel IRuby y me permitieron mostrar código vivo dentro de cada slide.

Este es el notebook que usé el día de la charla: http://nbviewer.ipython.org/github/jjconti/aprendiendo-ruby/blob/rubylit_meetup/RubyDesdePython.ipynb

Y esta la versión actualizada con los comentarios que surgieron durante la charla y que voy mejorando con el tiempo: http://nbviewer.ipython.org/github/jjconti/aprendiendo-ruby/blob/master/RubyDesdePython.ipynb

Gonzalo Martinez: Poderosa Edición Nro8 de #TheOriginalHacker

   Publicado:

Esta edición [0] es poderosa, con el estilo simple y profundo que la caracteriza, Eugenia nos redobla la apuesta con distintos análisis de alto nivel en lo profesional y de bajo nivel en lo computacional.










PHP y el manejo de datos en memoria
Los lenguajes de programación de alto nivel generalmente administran la memoria por nosotros y los más desinteresados crean y usan variables sin preocuparse demasiado pero si vos sos de aquellos a los que SI les importa ese tipo de manejos. Bueno este artiulo es para vos.

PHP: Hacking, debugging, o simplemente diversión
La diversión, como la belleza en la programación pueden parecer (para algunos) conceptos mal usados cuando se trata de un proceso que en gran proporción es racional, pero, la creatividad y la humanidad que cada uno le imprime a su manera de escribir código, nos hace retomar esos conceptos en su forma más pura. Divertirse debuggeando o hackeando eso es lo que Eugenia nos propone en este articulo.
«Belleza es la API de la librería requests de Kenneth Reitz para Python[1]»

Software Libre y la diferencia con el software privativo desde la óptica intrínsecamente profesional y no política
Huff, un articulo profundo, simplemente tremendo.

La orientación a objetos y sus enfoques como estilo y paradigma.
Amo este articulo (no es por que los ejemplos estén escritos en Python bueno un poco sí) por que nuevamente se puede ver como con palabras sencillas se puede llegar profundo en cosas que normalmente se explican en libros enormes. (que igualmente no habría que dejar de leer).

En síntesis:

Una edición tremenda donde encontrarás como dije al principio un alto nivel profesional y un bajo nivel computacional.

Y nuevamente me gustaría decirle a Eugenia como le dije por Twitter [2].
Gracias por decidir ser Libre.

[0] http://originalhacker.org/
[1] http://docs.python-requests.org/en/latest/
[2] https://twitter.com/GonzaMartinez/status/500384932536262656
Share