Back-End

10 jun, 2013

O mesmo aplicativo 4 vezes: PHP vs Python vs Ruby vs Clojure

Publicidade

Aqui está um programa de brincadeira que eu escrevi em PHP, Python, Ruby e Clojure. Espero que seja útil para alguém que sabe pelo menos uma dessas linguagens e quer aprender outra.

Esse programa é chamado de “Nurblizador” e faz apenas uma coisa: aceita qualquer texto e tenta substituir qualquer palavra que não seja um substantivo pela palavra “nurble”. Ele já está funcionando e rodando em http://nurblizer.herokuapp.com/.

Primeiro em PHP. O repositório pode ser encontrado em https://github.com/adambard/nurblizer-php.

Não sou um grande fã de PHP, mas não dá para discutir sobre a simplicidade de seu deployment. O repositório possui apenas seis arquivos.

O arquivo index.php contém apenas HTML porque, na verdade, ele não faz coisa alguma.

<?php include '_header.php'; ?>

<h1>Nurblizer</h1>
<form action="/nurble.php" method="post">
    <fieldset>
        <ul>
            <li>
                <label>Text to nurblize</label>
                <textarea name="text"></textarea>
            </li>
            <li>
                <input type="submit" value="Nurblize Away!">
            </li>
        </ul>
    </fieldset>
</form>
<p>
    <a href="http://www.smbc-comics.com/?id=2779">wtf?</a>
</p>

<?php include '_footer.php';

A mágica acontece no nurble.php

<?php

$nouns = file("nouns.txt", FILE_IGNORE_NEW_LINES);

function nurble($text){
    $text = strtoupper($text);
    $words = preg_split(
        '/\w/',
        preg_replace('/[^a-z ]/', '', strtolower($text)));

    foreach($words as $word){
        if(!in_array($word, $nouns)){
            $pattern = '/(\b)' . $word . '(\b)/i';
            $replacement = '\1<span class="nurble">nurble</span>\2';
            $text = preg_replace($pattern, $replacement, $text);
        }
    }

    return str_replace("\n", '<br>', $text);
}

include '_header.php'; ?>

<h1>Your Nurbled Text</h1>
<div><?php echo nurble($_POST['text']); ?></div>
<p>
    <a href="/">&lt;&lt; Back</a>
</p>

<?php include '_footer.php';

O que acontece aqui?

  • O arquivo nouns.txt é lido para produzir um array de substantivos.
  • O texto é convertido para maiúsculo.
  • A lista de palavras no texto é produzida ao converter as palavras para letra minúscula e depois separadas utilizando preg_split
  • É feito um loop pela lista e, se uma palavra estiver na lista de substantivos, cada instância daquela palavra no texto é substituída por um trecho de código HTML que mostra a palavra “nurble”.

Ok, isso parece funcionar (aviso: eu não quis instalar o Apache+PHP na minha máquina de desenvolvimento, portanto, não testei essa versão). Deployment em PHP funciona assim:

  • certifique-se de que o seu servidor está executando o apache e mod_php
  • ponha os arquivos onde o servidor espera encontrá-los

Então por que não paramos por aqui? Bem, há algumas objeções para serem feitas em relação ao código acima.

  1. O arquivo nouns.txt é necessariamente lido e separado a cada requisição, o que não é opcional.
  2. A linguagem é inconsistente em alguns lugares. É str_replace ou strtoupper?

Em seguida, queremos tentar em Ruby. Diferentemente do PHP, não é possível simplesmente jogar código em Ruby no servidor e esperar que ele funcione; você provavelmente irá desejar utilizar um framework para fazer a maior parte do trabalho para você.

Aqui entra o Sinatra. O Sinatra é um microframework em Ruby que faz um ótimo trabalho ao conservar as coisas simples. Você pode encontrar os exemplos do nurblizador aqui: https://github.com/adambard/Nurblizer

Como no Rails, seu irmão maior, o Sinatra não se importa em fazer algumas suposições ele mesmo. Ele entende que queremos nossos templates em views/ e nossos arquivos estáticos em public/

Chega. Aqui está o código:

require 'sinatra'

# configure block is a Sinatra feature
configure do
    @@nouns = File.open('nouns.txt').map{|line|
        line.strip.downcase
    }
end

def nurble(text)
    text = text.upcase
    words = text.downcase().gsub(/[^a-z ]/, '').split

    words.each{|w|
        if not @@nouns.include? w
          pattern = Regexp.new('(\b)'+ w + '(\b)', Regexp::IGNORECASE)
          replacement = "\1<span class=\"nurble\">nurble</span>\2"
          text.gsub! pattern, replacement
        end
    }
    text.gsub(/\n/, '<br>')
end

get "/" do
    haml :index
end

post "/nurble" do
    haml :nurble, :locals => {
      :text => nurble(params["text"])
    }
end

Então o que é diferente?

  • Agora carregamos a variável @@nouns apenas uma vez, no bloco de configuração, e podemos nos referir a ela a cada requisição.
  • Nossos templates estão resguardados no diretório views/ e ainda podemos utilizar HAML, o que é muito legal.
  • Nossas dependências podem ser declaradas no Gemfile do projeto, que pode ser facilmente instalado usando bundle install.
  • No geral, o código é bem mais “arejado”, não acham?

O pedaço relacionado às dependências é o mais interessante para mim. Em PHP, o problema das dependências fica a cargo do Apache e, por extensão, do servidor. O PEARS ajuda bastante, mas mesmo ele pode não estar disponível sempre. Em nosso projeto em Ruby, todas as dependências estão como de costume disponíveis para revisão e instaláveis via rubygems.

Suponha que iremos passar para Python agora. Talvez tenhamos restrições de desempenho e podemos imaginar que em Python seria mais rápido (me disseram que isso não é mais verdade no Ruby 2.0, portanto, coloque o pretexto que preferir acima).

Em Python, o melhor microframework é o Flask, até onde eu sei. Há muitos outros, mas eles não possuem implementações de nurblizadores feitas com ele. Aqui está o código que utilizaremos: https://github.com/adambard/py-nurblizer

O Flask coloca os templates em templates/ e arquivos estáticos em static/. No resto, é igual ao Sinatra (mas em Python).

import re
from flask import Flask, render_template, request
app = Flask(__name__)

# Read in the nouns file
with open("nouns.txt") as f:
    NOUNS = [l.strip().lower() for l in f]

# Nurblize!
def nurble(text):
    text = text.upper()
    words = re.sub(r'[^a-z ]', '', text.lower()).split()

    for word in words:
        if word not in NOUNS:
            pattern = r'(\b)' + word + r'(\b)'
            replacement = r'\1<span class="nurble">nurble</span>\2'
            text = re.sub(pattern, replacement, text, flags=re.I)

    return text.replace('\n', '<br>')

@app.route("/", methods=['GET'])
def index_view():
    return render_template("index.html")

@app.route("/nurble", methods=['POST'])
def nurble_view():
    return render_template(
        "nurble.html",
        text=nurble(request.form.get('text', '')))

if __name__ == "__main__":
    app.run()

Note que a abordagem do Flask é ser uma linguagem menos específica para aplicativos web do que é o Sinatra. Em vez de blocos get e post, temos decoração de roteamento em antigas funções do Python. Note que, no Flask, definimos um aplicativo como objeto, acrescentamos as rotas a ele e o executamos explicitamente no final do arquivo.

Ok, então agora o nosso aplicativo nurblizador está ficando realmente popular e precisamos portá-lo para o Clojure. Por quê? Porque eu já escrevi um exemplo, droga! Aqui está: https://github.com/adambard/nurblizer-clj

Usar o Clojure para esse aplicativo é como utilizar um canhão para matar uma mosca, mas ele tem algumas propriedades bem legais. Ele trata todos os dados como imutáveis, a não ser que você faça de outra forma, o que é ótimo para paralelismo. A linguagem roda na  JVM, que é  amplamente disponível e é muito rápida também.

Escrever aplicativos web em Clojure é um pouco diferente dos outros microframeworks em outras linguagens. O Noir, um framework Clojure, não está mais sendo mantido, contudo não escreveremos muito mais código se acrescentarmos nosso próprio microframework a partir dos componentes disponíveis. Então faremos isso!

(ns nurblizer.core
  (:gen-class :name nurblizer.core)
  (:use compojure.core nurblizer.helpers)
  (:require
    [clojure.string :as str]
    [ring.adapter.jetty :as ring]
    [compojure.core :as compojure]
    [compojure.route :as route]
    [compojure.handler :as handler]))

; Read in the nouns file on startup
(def nouns
  (map (comp str/trim str/lower-case)
       (-> (slurp (clojure.java.io/resource "nouns.txt"))
           (str/split #"\n"))))

; Nurblize function: now with recursion!
(defn nurble
  ([text]
  ; First run: prepare the wordlist and upper-case the text.
   (let [words (-> text
                   str/lower-case
                   (str/replace #"[^a-z ]" "")
                   (str/split #"\s"))]
     (nurble (str/upper-case text) words)))

  ([text words]
  ; Recursively update <text> by replacing each <word> of <words> iff <word> is in nouns
   (if (not (empty? words))
     (let [w (first words)
           pattern (re-pattern (str "(?i)(\\b)" w "(\\b)"))
           replacement "$1<span class=\"nurble\">nurble</span>$2"
           text (if (not (some (partial = w) nouns))
                  (str/replace text pattern replacement)
                  text)]
       (recur text (rest words)))
     (str/replace text #"\n" "<br>"))))

; Define handlers
(defn index-view []
  (render "index" {}))

(defn nurble-view [text]
  (render "nurble" {:text (nurble text)}))

; Routes
(defroutes main-routes
  (GET "/" [] (index-view))
  (POST "/nurble" [text] (nurble-view text))
  (route/resources "/static"))

; And finally, the server itself
(defn -main []
  (ring/run-jetty (handler/site main-routes) {:port 9000}))

A primeira coisa que você irá notar é que importaremos muito mais bibliotecas do que nas outras linguagens. Estaremos utilizando tudo que está disponível em compjure.core e nurblizer.helpers (o código fonte disso está logo abaixo). Usamos o Ring e o adaptador Jetty para executar o servidor, o Compojure para manipular o roteamento e para conectá-lo ao Ring. Também utilizaremos o Clostache, uma implementação do Mustache, para renderizar os templates; essa é a função de renderização no final de cada view. Usaremos também o Leiningen como gerenciador do projeto/solucionador de dependências.

A função nurblizer é bem mais imponente aqui. Não queremos colocar o texto no lugar das palavras originais da mesma forma como fizemos com os outros 3 exemplos, por causa da imutabilidade mencionada anteriormente. É claro que sempre podemos contar com os objetos e métodos em Java, mas nesse caso estaríamos escrevendo em Java. Em vez disso, iremos atualizar o texto recursivamente, uma palavra de cada vez.

Aqui está a função de renderização:

(ns nurblizer.helpers
  (:require
    [clostache.parser :as clostache]))

(defn read-template [template-file]
  (slurp (clojure.java.io/resource (str "templates/" template-file ".mustache"))))

; Quick-and-dirty Mustache renderer.
(defn render
  ([template-file params]
   (clostache/render (read-template template-file) params
      {:_header (read-template "_header")
       :_footer (read-template "_footer") })))

Mas por que Clojure? Bem, para início de conversa, seu desempenho deve ser o melhor de todos. Isso é duplamente verdade caso você utilize um servidor como o httpkit, que eu testei com até 100 conexões simultâneas em uma única instância do Heroku. Você pode empacotá-lo utilizando lein uberjar e ter um único arquivo, o qual pode ser executado em qualquer lugar que tenha Java.

Tudo isso faz com que compense utilizar Clojure para esse aplicativo bobo? Na minha opinião, claro que não. A versão em Ruby é muito mais fácil de ler e bastante poderosa. Mas utilizar Clojure logo de início pode economizar problemas caso seu projeto precise ser expandido no futuro. Também é muito divertido, mas essa é a minha opinião pessoal.

Este artigo gerou uma discussão de qualidade no Hacker News, então dê uma olhada por lá em: https://news.ycombinator.com/item?id=5440170

Quanto ao algoritmo do nurblizador: muitas melhorias foram propostas. A mais óbvia delas é utilizar um conjunto ou hash table para armazenar os substantivos. Eu encorajo qualquer um a mergulhar mais fundo e enviar pull requests em meus repositórios para o nurblizador.

Algumas pessoas também contribuíram com implementações em outras linguagens:

GO

JAVASCRIPT (NODE.JS)

JSP

greyrest, do Hacker News, fez esta versão em Clojure, que é a minha favorita até agora (e preserva os espaços para boot). Eu a coloco aqui, porque penso que meus esforços para espelhar as outras implementações na versão Clojure não representam a linguagem tão bem quanto ela merece.

(ns nurblizer.core
      (:gen-class)
      (:use compojure.core)
      (:require
        [clojure.string :as str]
        [clostache.render :as clostache]
        [ring.adapter.jetty :only run-jetty]
        [compojure.handler :only site]))

    ;; main nurble stuff
    (def nouns
      (->> (-> (slurp (clojure.java.io/resource "nouns.txt")) ; read in nouns.txt
               (str/split #"\n"))                             ; split by line
           (map (comp str/trim str/upper-case))               ; feed the lines through upper-case and trim
           set))                                              ; transform into a set

    (def nurble-replacement-text "<span class=\"nurble\">nurble</span>")

    (defn nurble-word [word]
      (get nouns (str/upper-case word) nurble-replacement-text)) ; return word if word in set else nurble

    (defn nurble [text]
      (str/replace text #"\n|\w+" #(case %               ; using anon func literal, switch on argument
                                      "\n" "<br>"        ; when arg is newline replace with br
                                      (nurble-word %)))) ; otherwise nurble the argument (a word)

    ;; webserver stuff
    (defn read-template [template-file]
      (slurp (clojure.java.io/resource (str "templates/" template-file ".mustache"))))

    (defn render
      ([template-file params]
       (clostache/render (read-template template-file) params
                         {:_header (read-template "_header")
                          :_footer (read-template "_footer") })))

    (defroutes main-routes
      (GET "/"        []     (render "index" {}))
      (POST "/nurble" [text] (render "nurble" {:text (nurble text)})))

    (defn -main []
      (run-jetty (site main-routes) {:port 9000}))

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://adambard.com/blog/PHP-ruby-python-clojure-webapps-by-example/