Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ruby Ate My DSL!

Daniel Azuma
November 18, 2019

Ruby Ate My DSL!

Presentation by Daniel Azuma at RubyConf 2019. Covers how DSLs can clash with "normal" Ruby code, and techniques for hardening a DSL against such conflicts.

Daniel Azuma

November 18, 2019
Tweet

More Decks by Daniel Azuma

Other Decks in Programming

Transcript

  1. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  2. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  3. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  4. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  5. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end LocalJumpError: no block given (yield) .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' shell_app.rb:7:in `block in <main>' .../lib/sinatra/base.rb:1635:in `call' .../lib/sinatra/base.rb:1635:in `block in compile!' .../lib/sinatra/base.rb:987:in `block (3 levels) in route!' .../lib/sinatra/base.rb:1006:in `route_eval' .../lib/sinatra/base.rb:987:in `block (2 levels) in route!' .../lib/sinatra/base.rb:1035:in `block in process_route' .../lib/sinatra/base.rb:1033:in `catch' .../lib/sinatra/base.rb:1033:in `process_route' .../lib/sinatra/base.rb:985:in `block in route!' .../lib/sinatra/base.rb:984:in `each' .../lib/sinatra/base.rb:984:in `route!' .../lib/sinatra/base.rb:1097:in `block in dispatch!' .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' .../lib/sinatra/base.rb:1094:in `dispatch!' .../lib/sinatra/base.rb:919:in `block in call!' ...
  6. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end LocalJumpError: no block given (yield) .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' shell_app.rb:7:in `block in <main>' .../lib/sinatra/base.rb:1635:in `call' .../lib/sinatra/base.rb:1635:in `block in compile!' .../lib/sinatra/base.rb:987:in `block (3 levels) in route!' .../lib/sinatra/base.rb:1006:in `route_eval' .../lib/sinatra/base.rb:987:in `block (2 levels) in route!' .../lib/sinatra/base.rb:1035:in `block in process_route' .../lib/sinatra/base.rb:1033:in `catch' .../lib/sinatra/base.rb:1033:in `process_route' .../lib/sinatra/base.rb:985:in `block in route!' .../lib/sinatra/base.rb:984:in `each' .../lib/sinatra/base.rb:984:in `route!' .../lib/sinatra/base.rb:1097:in `block in dispatch!' .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' .../lib/sinatra/base.rb:1094:in `dispatch!' .../lib/sinatra/base.rb:919:in `block in call!' ...
  7. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  8. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  9. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end method added to the “main” object
  10. # routing.rb Rails.application.routes.draw do resources :brands, only: [:index, :show] do

    resources :products, only: [:index, :show] end resource :basket, only: [:show, :update, :destroy] resolve("Basket") { route_for(:basket) } end
  11. # routing.rb Rails.application.routes.draw do resources :brands, only: [:index, :show] do

    resources :products, only: [:index, :show] end resource :basket, only: [:show, :update, :destroy] resolve("Basket") { route_for(:basket) } end methods of the class ActionDispatch::Routing::Mapper
  12. # routing.rb Rails.application.routes.draw do resources :brands, only: [:index, :show] do

    resources :products, only: [:index, :show] end resource :basket, only: [:show, :update, :destroy] resolve("Basket") { route_for(:basket) } end methods of the class ActionDispatch::Routing::Mapper uses instance_eval to set self to a mapper instance
  13. DSL howto: • Add methods to an existing object •

    Change “self” within a block
  14. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  15. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end “describe” block
  16. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end “describe” block spec
  17. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb
  18. # nanospec.rb module Kernel def describe name, &block end end

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  19. # nanospec.rb class Entity def initialize name @name = name

    end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity end end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  20. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb class Entity def initialize name @name = name end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity end end
  21. # nanospec.rb class Entity def initialize name @name = name

    end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  22. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb class Entity def initialize name @name = name end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end
  23. # nanospec.rb class Entity def initialize name @name = name

    end def it name, &block end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  24. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb class Entity def initialize name @name = name @specs = {} end def it name, &block @specs[name] = block end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end
  25. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  26. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  27. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb class Entity def initialize name @name = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end
  28. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do @name = "Ruby" puts "My name is #{@name}" end it "sleeps" do puts "zzz" end end
  29. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do @name = "Ruby" puts "My name is #{@name}" end it "sleeps" do puts "zzz" end end
  30. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end LocalJumpError: no block given (yield) .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' shell_app.rb:7:in `block in <main>' .../lib/sinatra/base.rb:1635:in `call' .../lib/sinatra/base.rb:1635:in `block in compile!' .../lib/sinatra/base.rb:987:in `block (3 levels) in route!' .../lib/sinatra/base.rb:1006:in `route_eval' .../lib/sinatra/base.rb:987:in `block (2 levels) in route!' .../lib/sinatra/base.rb:1035:in `block in process_route' .../lib/sinatra/base.rb:1033:in `catch' .../lib/sinatra/base.rb:1033:in `process_route' .../lib/sinatra/base.rb:985:in `block in route!' .../lib/sinatra/base.rb:984:in `each' .../lib/sinatra/base.rb:984:in `route!' .../lib/sinatra/base.rb:1097:in `block in dispatch!' .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' .../lib/sinatra/base.rb:1094:in `dispatch!' .../lib/sinatra/base.rb:919:in `block in call!' ...
  31. DSL

  32. TIP: Use a naming convention for instance variables and private

    methods # nanospec.rb class Entity def initialize name @name = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end
  33. # nanospec.rb class Entity def initialize name @__name = name

    @__specs = {} end def it name, &block @__specs[name] = block end def check @__specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end TIP: Use a naming convention for instance variables and private methods
  34. # nanospec.rb class Entity def initialize name @__name = name

    @__specs = {} end def it name, &block @__specs[name] = block end def __check @__specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end TIP: Use a naming convention for instance variables and private methods
  35. TIP: # nanospec.rb class Entity def initialize name @name =

    name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end both the DSL and its implementation Delegate implementation to a separate object
  36. # nanospec.rb class EntityImpl def initialize name @name = name

    @specs = {} end def add name, block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end TIP: class EntityDSL def initialize impl @__impl = impl end def it name, &block @__impl.add name, block end end module Kernel def describe name, &block entity = EntityImpl.new name (@entities ||= []) << entity entity_dsl = EntityDSL.new entity entity_dsl.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end DSL only Implementation only Delegate implementation to a separate object
  37. TIP: class EntityDSL def initialize impl @__impl = impl end

    def it name, &block @__impl.add name, block end end module Kernel def describe name, &block entity = EntityImpl.new name (@entities ||= []) << entity entity_dsl = EntityDSL.new entity entity_dsl.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end DSL only # nanospec.rb class EntityImpl def initialize name @name = name @specs = {} end def add name, block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end Implementation only Delegate implementation to a separate object
  38. # nanospec.rb class EntityImpl def initialize name @name = name

    @specs = {} end def add name, block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end Implementation only TIP: class EntityDSL def initialize impl @__impl = impl end def it name, &block @__impl.add name, block end end module Kernel def describe name, &block entity = EntityImpl.new name (@entities ||= []) << entity entity_dsl = EntityDSL.new entity entity_dsl.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end DSL only Delegate implementation to a separate object
  39. # nanospec.rb class EntityImpl def initialize name @name = name

    @specs = {} end def add name, block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end TIP: class EntityDSL def initialize impl @__impl = impl end def it name, &block @__impl.add name, block end end module Kernel def describe name, &block entity = EntityImpl.new name (@entities ||= []) << entity entity_dsl = EntityDSL.new entity entity_dsl.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end DSL only Implementation only Delegate implementation to a separate object
  40. DSL

  41. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  42. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  43. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module NanospecDSL def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end extend NanospecDSL at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  44. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module NanospecDSL def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end extend NanospecDSL at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end TIP: Add methods only where needed
  45. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  46. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end end def try_speaking puts "testing 1...2...3..." end
  47. private method of Object # nanospec_test.rb require "./nanospec.rb" describe "foo"

    do it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end
  48. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end ... subclass of Object # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end
  49. # nanospec.rb class Entity < BasicObject def initialize name @name

    = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end ... # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end BasicObject?
  50. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end end def try_speaking puts "testing 1...2...3..." end FAIL # nanospec.rb class Entity < BasicObject def initialize name @name = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end ... BasicObject?
  51. # nanospec.rb class Entity < BasicObject def initialize name @name

    = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end ... # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end TIP: BasicObject is usually NOT an effective base class for a DSL
  52. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end end def try_speaking puts "testing 1...2...3..." end
  53. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end def try_speaking puts "testing 1...2...3..." end end
  54. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end def try_speaking puts "testing 1...2...3..." end end TIP: Design DSLs to honor lexical scoping
  55. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end def try_speaking puts "testing 1...2...3..." end end TIP: Design DSLs to honor lexical scoping method of singleton class block executed with instance_eval
  56. TIP: Design DSLs to honor lexical scoping # nanospec_test.rb require

    "./nanospec.rb" describe "foo" do it "speaks" do try_speaking end def try_speaking puts "testing 1...2...3..." end end describe "bar" do it "speaks" do try_speaking end def try_speaking puts "I'm a different object." end end separate scopes
  57. # nanospec_test.rb require "./nanospec.rb" describe "outer" do describe "inner" do

    it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end end TIP: Design DSLs to honor lexical scoping nested block
  58. # nanospec_test.rb require "./nanospec.rb" describe "outer" do describe "inner" do

    it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end end TIP: Design DSLs to honor lexical scoping delegate methods
  59. # nanospec_test.rb require "./nanospec.rb" describe "bar" do it "speaks" do

    try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end
  60. Each “describe” block creates a class # nanospec_test.rb require "./nanospec.rb"

    describe "bar" do it "speaks" do try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end TIP: Consider modeling blocks as classes
  61. Each test is executed within a separate instance # nanospec_test.rb

    require "./nanospec.rb" describe "bar" do it "speaks" do try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end TIP: Consider modeling blocks as classes
  62. # nanospec_test.rb require "./nanospec.rb" describe "bar" do it "speaks" do

    try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end Method of the described class TIP: Consider modeling blocks as classes
  63. # nanospec_test.rb require "./nanospec.rb" describe "bar" do it "speaks" do

    try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end Method of the described class Use class_eval instead of instance_eval TIP: Consider modeling blocks as classes
  64. TIP: Consider modeling blocks as classes # nanospec_test.rb require "./nanospec.rb"

    describe "bar" do it "speaks" do try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end
  65. ≈ ≈ # nanospec_test.rb require "./nanospec.rb" describe "foo" do MESSAGE

    = "Hello world!" it "speaks" do puts MESSAGE end end describe "bar" do MESSAGE = "Hello RubyConf!" it "speaks" do puts MESSAGE end end
  66. ≈ # nanospec_test.rb require "./nanospec.rb" describe "foo" do MESSAGE =

    "Hello world!" it "speaks" do puts MESSAGE end end describe "bar" do MESSAGE = "Hello RubyConf!" it "speaks" do puts MESSAGE end end Constant redefined warning (both constants defined on Object)
  67. TIP: Provide alternatives to constants # rspec_example.rb require "rspec" describe

    "foo" do let(:message) { "Hello, world!" } it "speaks" do puts message end end describe "bar" do let(:message) { "Hello, RubyConf!" } it "speaks" do puts message end end
  68. DSL

  69. Use a naming convention for instance variables and private methods

    Delegate implementation to a separate object DSL
  70. Add methods only where needed BasicObject is usually NOT an

    effective base class for a DSL Design DSLs to honor lexical scoping Consider modeling blocks as classes ❤ DSL
  71. DSL