Una historia de ds ls en ruby

45
Leo Soto M. Continuum Lenguajes Dinámicos, Enero 2013 Una historia de DSLs en Ruby Saturday, February 9, 13
  • date post

    20-Oct-2014
  • Category

    Technology

  • view

    377
  • download

    1

description

Casos de uso y tips de implementacion de DSLs en Ruby Presentacion hecha en el meetup lenguajes dinamicos, Enero 2013

Transcript of Una historia de ds ls en ruby

Page 1: Una historia de ds ls en ruby

Leo Soto M.ContinuumLenguajes Dinámicos, Enero 2013

Una historia de DSLs en Ruby

Saturday, February 9, 13

Page 2: Una historia de ds ls en ruby

DSLs?

Saturday, February 9, 13

Page 3: Una historia de ds ls en ruby

Muchas libs Ruby son en el fondo, DSLs

Saturday, February 9, 13

Page 4: Una historia de ds ls en ruby

Capistrano

namespace :deploy do task :start, :roles => :app, :except => { :no_release => true } do run "sudo monit start miapp_unicorn" run "sudo monit -g resque start all" endend

Saturday, February 9, 13

Page 5: Una historia de ds ls en ruby

describe Math do describe "#pow" do it "computes the n-th-power of the receiver" do 3.pow(3).should == 27 end endend

RSpec

Saturday, February 9, 13

Page 6: Una historia de ds ls en ruby

Bundler

gem 'rails', '3.2.11'gem 'pg'gem 'jquery-rails'gem "unicorn", ">= 4.3.1"gem "haml", ">= 3.1.7"gem "devise", ">= 2.1.2"gem "simple_form", ">= 2.0.4"gem "figaro", ">= 0.5.0"gem "foreigner", ">= 1.3.0"

group :assets do gem 'sass-rails', '~> 3.2.3' gem 'compass-rails' gem 'coffee-rails', '~> 3.2.1' gem 'uglifier', '>= 1.0.3'end

Saturday, February 9, 13

Page 7: Una historia de ds ls en ruby

Sinatra

require 'sinatra'

get '/hi' do "Hello World!"end

Saturday, February 9, 13

Page 8: Una historia de ds ls en ruby

Routing en Rails

Saturday, February 9, 13

Page 9: Una historia de ds ls en ruby

Modelos en Rails

Saturday, February 9, 13

Page 10: Una historia de ds ls en ruby

Casi todo en Rails

Saturday, February 9, 13

Page 11: Una historia de ds ls en ruby

Lo que en J__A “resuelven” con X_Len Ruby se suele hacer con DSLs internos

Saturday, February 9, 13

Page 12: Una historia de ds ls en ruby

Enter Pharmmd Rules

Saturday, February 9, 13

Page 13: Una historia de ds ls en ruby

age  >=  18  and  (drug_cat(“XYZ”)  or                                drug_cat(“ABC”))and  dosage(“PARACETAMOL”)  >  1000

Saturday, February 9, 13

Page 14: Una historia de ds ls en ruby

Solución:

Saturday, February 9, 13

Page 15: Una historia de ds ls en ruby

# patient_dsl.rbclass PatientDSL

attr_reader :patient

def initialize(patient) @patient = patient end

delegate :age, :allergies, :gender, :labs, :medications, :prescriptions, :providers, :visits, :survey, :to => :patient

def drug_cat(name) (1..6).to_a.reverse.detect do |level| medication = medications.detect do |m| m.send("lvl#{level}conceptname") == name end and break medication end end

def dosage(name) drug(name).try(:dosage) || 0 end

# Many, many more methods

end

Saturday, February 9, 13

Page 16: Una historia de ds ls en ruby

PatientDSL.new(patient).instance_eval(rule)

Saturday, February 9, 13

Page 17: Una historia de ds ls en ruby

Recuerden, las reglas las ingresas usuarios, en una gran caja de texto en la interfaz web del sistema

¿Que problemas pueden ocurrir con la solución hasta ahora?

Saturday, February 9, 13

Page 18: Una historia de ds ls en ruby

age  >=  18  and  patient.destroy!

Saturday, February 9, 13

Page 19: Una historia de ds ls en ruby

Patient.destroy_all

Saturday, February 9, 13

Page 20: Una historia de ds ls en ruby

system(“rm  -­‐rf  /”)

Saturday, February 9, 13

Page 21: Una historia de ds ls en ruby

PatientDSL.new(patient).instance_eval(rule)

Saturday, February 9, 13

Page 22: Una historia de ds ls en ruby

“Cuando me equivoco en tipear la reglame aparece un error ‘No Method no se cuantito’”

Saturday, February 9, 13

Page 23: Una historia de ds ls en ruby

Solución, versión 2:

Saturday, February 9, 13

Page 24: Una historia de ds ls en ruby

# pharmmd_dsl.treetop

grammar PharmmdDsl rule expression spaces? boolean_expression spaces? end

rule boolean_expression boolean_term (spaces logical_binary_operator spaces boolean_term)* end

rule logical_binary_operator "and" / "&&" / "or" / "||" end

rule boolean_term ("not " / "!") spaces? boolean_expression / (numeric_value spaces? boolean_operator spaces? numeric_value) / ("(" boolean_expression ")") / function_call end # ...

Saturday, February 9, 13

Page 25: Una historia de ds ls en ruby

# pharmmd_dsl.treetop (cont.) rule boolean_operator ">=" / "<=" / ">" / "<" / "==" / "!=" end

rule function_call function_name:([a-zA-Z_] [a-zA-Z_0-9]*) arguments:("(" argument ("," spaces? argument)* ")")? <FunctionNode> end

rule argument string / date / numeric_value end

Saturday, February 9, 13

Page 26: Una historia de ds ls en ruby

# pharmmd_dsl.treetop (cont.)

rule numeric_value function_call / number / "(" spaces? numeric_value spaces? ")" end

rule number float / integer end

rule integer "-"? digits end

rule float "-"? (digits)? "." digits end

rule digits [0-9]+ end

Saturday, February 9, 13

Page 27: Una historia de ds ls en ruby

# pharmmd_dsl.treetop (cont.)

rule spaces [\s\n]+ end

rule string ['"] [^'"]* ['"] end

rule date [0-9]+ "." time_unit "s"? ".ago" end

rule time_unit "day" / "month" / "year" end

end

Saturday, February 9, 13

Page 28: Una historia de ds ls en ruby

Por cierto, el lenguaje de gramáticas de treetop es un DSL “externo”

Saturday, February 9, 13

Page 29: Una historia de ds ls en ruby

Treetop hace el parsing extremadamente natural.

¡Sigamos el proceso a mano!

(si es que tenemos pizarra a mano)

Saturday, February 9, 13

Page 30: Una historia de ds ls en ruby

age  >=  18  and  (drug_cat(“XYZ”)  or                                drug_cat(“ABC”))and  dosage(“PARACETAMOL”)  >  1000

Saturday, February 9, 13

Page 31: Una historia de ds ls en ruby

¿Pero qué ganamos?

Saturday, February 9, 13

Page 32: Una historia de ds ls en ruby

age  >=  18  and  patient.destroy  #  invalidoPatient.destroy_all  #  invalidosystem(“rm  -­‐rf  /”)  #  valido!

Saturday, February 9, 13

Page 33: Una historia de ds ls en ruby

eval(“Patient.destroy_all”)  #  oops

Saturday, February 9, 13

Page 34: Una historia de ds ls en ruby

¡Rara vez la validación sintáctica es suficiente!

“Saltarina casa llovió perros perrunos” (¡Español sintácticamente válido!)

Saturday, February 9, 13

Page 35: Una historia de ds ls en ruby

rule function_call function_name:([a-zA-Z_] [a-zA-Z_0-9]*) arguments:("(" argument ("," spaces? argument)* ")")? <FunctionNode> end

Saturday, February 9, 13

Page 36: Una historia de ds ls en ruby

require 'treetop'; require 'pharmmd_dsl'

class FunctionNode < Treetop::Runtime::SyntaxNode; end

class PharmmdDslValidator attr_accessor :dsl, :errors

def initialize(dsl) @dsl = dsl; @errors = [] end

def valid_dsl? parser = PharmmdDslParser.new parse_tree = parser.parse(@dsl) if parse_tree.nil? errors << "You have a syntax error: #{parser.failure_reason}" else validate_functions(parse_tree) end errors.empty? end

def valid_functions @valid_functions ||= (PatientDSL.instance_methods - Object.instance_methods) end

Saturday, February 9, 13

Page 37: Una historia de ds ls en ruby

# (cont.)

def validate_functions(parse_tree) element = parse_tree if element.is_a? FunctionNode name = element.function_name.text_value unless valid_functions.include? name errors << ("Function name #{element.text_value} is not a valid function call") end end if element.elements parse_tree.elements.each do |element| validate_functions(element) end end endend

Saturday, February 9, 13

Page 38: Una historia de ds ls en ruby

age  >=  18  and  patient.destroy  #  invalidoPatient.destroy_all  #  invalidosystem(“rm  -­‐rf  /”)  #  invalido!

Saturday, February 9, 13

Page 39: Una historia de ds ls en ruby

Errores amigables:

“Se esperaba ‘(‘ en linea X, columna Y”

“La función ‘system’ no es válida”

Saturday, February 9, 13

Page 40: Una historia de ds ls en ruby

Y mucho más: ¡Solucion v3!(Sólo una mirada rápida, que se nos acaba el tiempo)

Saturday, February 9, 13

Page 41: Una historia de ds ls en ruby

¡DSL híbrido!

Saturday, February 9, 13

Page 42: Una historia de ds ls en ruby

(quantity("lipitor") > 10).or(drug("vicodin"))

quantity("lipitor") > 10 or drug("vicodin")

(PharmdDSLValidator)

(PharmdDSLPreProcessor)

(PharmdDSL)

(NumericExpr(20, ["Lipitor 50mg"]) > 10).or( BooleanExpr(true, ["Vicodin 20mg"]))

quantity("lipitor") > 10 or drug("vicodin")

Exte

rnal

D

SLIn

tern

al

DSL

tre

eto

p,

pars

e t

ree

sru

by o

bjs

/me

tod

os

Saturday, February 9, 13

Page 43: Una historia de ds ls en ruby

(quantity("lipitor") > 10).or(drug("vicodin"))

quantity("lipitor") > 10 or drug("vicodin")

(PharmdDSLValidator)

(PharmdDSLPreProcessor)

(DenominatorQuery)

(OrCondition( Condition('lipitor', {'$gt' => '10'}), Condition('vicodin', {'$exists' => true}))

quantity("lipitor") > 10 or drug("vicodin")

Exte

rnal

D

SLIn

tern

al

DSL

tre

eto

p,

pars

e t

ree

sru

by o

bjs

/me

tod

os

Saturday, February 9, 13

Page 44: Una historia de ds ls en ruby

MongoDB no tenía OR en esa época, por lo que optimizabamos el árbol de expresiones para

dejar los ORs lo mas “arriba” posible.

Ejemplo:

((X  or  Y)  and  (Z)

((X  and  Z)  or  (Y  and  Z))

...y mas

(Condition#optimize)

Saturday, February 9, 13

Page 45: Una historia de ds ls en ruby

Conclusión

• Parsing, árboles de expresiones, compiladores, etc no fue tiempo perdido en la U :)

• Pero siempre hay tradeoffs

• Probar un DSL interno primero. La solución más simple que podría funcionar

• Luego un DSL externo, treetop lo hace fácil

• Finalmente un híbrido, si no queda otraSaturday, February 9, 13