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="/"><< 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.
- O arquivo nouns.txt é necessariamente lido e separado a cada requisição, o que não é opcional.
- 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/