第16章 オリジナル言語を作る: Domain-Specific Languages
Ruby
Published: 2019-07-02

「Rubyによるデザインパターン」の読書メモです。

「内部」DSL

命令文をDSLの中に取り込んで、それをRubyプログラムとして解釈するアイディア

backup.pr

backup '/home/russ/documents'
backup '/home/russ/music', file_name('*.mp3') & file_name('*.wav')
backup '/home/russ/images', except(file_name('*.tmp'))
to '/external_drive/backups'

interval 60

test.rb

require 'finder'
require 'find'

class Expression
  def |(other)
    Or.new(self, other)
  end

  def &(other)
    And.new(self, other)
  end
end

class All < Expression
  def evaluate(dir)
    results = []
    Find.find(dir) do |p|
      next unless File.file?(p)
      results << p
    end
    results
  end
end

class And < Expression
  def initialize(expression1, expression2)
    @expression1 = expression1
    @expression2 = expression2
  end

  def evaluate(dir)
    result1 = @expression1.evaluate(dir)
    result2 = @expression2.evaluate(dir)
    (result1 & result2)
  end
end

class Not < Expression
  def initialize(expression)
    @expression = expression
  end

  def evaluate(dir)
    All.new.evaluate(dir) - @expression.evaluate(dir)
  end
end

class FileName < Expression
  def initialize(pattern)
    @pattern = pattern
  end

  def evaluate(dir)
    results = []
    Find.find(dir) do |p|
      next unless File.file?(p)
      name = File.basename(p)
      results << p if File.fnmatch(@pattern, name)
    end
    results
  end
end

def backup(dir, find_expression=All.new)
  puts "Backup called, source dir=#{dir} find expr=#{find_expression}"
end

def to(backup_directory)
  puts "To called, backup dir=#{backup_directory}"
end

def interval(minutes)
  puts "Interval called, interval = #{minutes} minutes"
end

def except(expression)
  Not.new(expression)
end

def file_name(pattern)
  FileName.new(pattern)
end

eval(File.read('backup.pr'))

まとめ

内部DSLの背景にある考え方は、とても簡単です。Rubyの構文ルールに合うようにあなたがDSLを定義します。そして、DSLで書かれた、やるべきことが記述されているプログラムに必要な基盤もあなたが定義します。