Ruby in Seven languages

총평

coffeescript를 쓰는 지라, 어색함은 덜했던 것 같네요. coffeescript의 출현도 이해가 가고, 그러한 유려함을 따질 때 javascript는 java만큼이나 ceremeny code가 많은 것도 사실이네요. 일단, code block의 매력적임을 부정할 수 없습니다. 특히, 중첩된 code block의 경우, nested lambda가 허용되지 않는 언어들이 생각났습니다. 물론, 가독성 논쟁이 있겠지만, 간단한 구문을 축약해서 표현하는 것은 표현력이 좋다고 말할 수 있겠습니다. 그 외에도, 동적인 요소들이 눈에 많이 들어 왔는데, method_missing은 재밋는 기능이었습니다. 정적 언어에서는 이미 불가한 이야기이고, 동적 언어에서도 지원하지 않는 기능이라 신기했습니다. 다만, 구현에 있어서, 결과적으로 어느정도 오버헤드를 가질 것이라는 생각이 드는 것도 사실입니다. 다만, 문법적 한계를 가지고 있는 언어들에 비해서, 더욱 동적인 입장을 취하고 있기에, meta programming에 강점을 가지고 있다고 생각합니다. 최종적으로 루비는 중세 시대의 기병같은 느낌을 주었습니다. 여러 특장점을 가지고 있고, 대형 application에도 부족함이 없을 것으로 생각됩니다만, 저의 경우에는 shell program이나 DSL을 중점적으로 사용할만한 layer에 사용하는 것이 좋을 것 같습니다. 예전에는 Ruby vs Python이라는 구도로 생각한 적이 많았는데, 분명히 Python과는 또 다른 매력을 가지고 있다는 사실에 진작 공부할 거라는 생각이 들기도 하네요.

logs

  • 7 Oct 2015
    • 팀장님이 없는 틈을 타서, 공부를!
  • 19 Nov
    • 다시 펴는데 1달이 넘게 걸렸네...

The first day

Intro

  • Dynamic Typing : 자료형이 컴파일 시간이 아닌 실행 시간에 결정
  • 마츠(루비 창시자)는 Block을 가장 마음에 든다고 했음
  • 개별적인 숫자를 포함한 거의 모든 것이 객체
  • nil과 false를 제외한 모든 것은 true(even though, 0)
  • logical operator : & , |
  • conditional logical operation : and, &&, or, ||
  • Strong typing : 코드가 실행되는 시점에서 type에 대한 검사를 실시

Self-Study : Exersize

  1. Print the string "Hello, world" puts "Hello, world"
  2. Find the index "Hello, Ruby".index("Ruby")
  3. Print your name "puts Ethan " * 10
  4. Print the string "This is sentence number 1," (1..10).each {|i| puts "This is sentence number #{i}"}
  5. Run a program from a file require "./firstday.rb"
  6. random number
def guess
  result, input = rand(10), nil
  while result != input
    input = gets().to_i
    puts 'less' if (input > result)
    puts 'more' if (input < result)
  end
end

The second day

Array

animals = ['lions', 'tigers', 'bears']

animals[4] # out of index, return nil
=> nil 
animals[-1] # negative index as reversed index
=> "bears"
animals[0..1] # range index
=> ["lions", "tigers"]
  • not homogeneous : 여러 타입을 가질 수 있음

Hash

stuff = { 1 => "one", :on => :off}

stuff[1]
=> "one"
stuff[:on]
=> :off
  • symbol 지원 (일종의 atom)

Code block

3.times {put 'hello'}

[1].each {|param| puts "Wow ##{param}"}
  • code block은 anonymous function의 단순화 형태
  • parameter를 지원
  • 구현 시, yield 혹은 &param으로 수행 가능

Class

  • Class name's CamelCase
  • Instance variables are needed a prefix of '@'
  • Class variables are needed a prefix of '@@'
  • All kind of method's name is underscore_style and lower cases
  • Constant's name composed by capital characters
  • Predication functions have a postfix of '?'

Mix-in

module Animal
  def die
  ...

class Person
  include Animal
  ...
  • 루비에서는 mix-in을 바탕으로 공통 함수를 공유한다.
  • mixin : 공통 적인 함수들을 하나의 묶음(module)으로 정의하고, 이를 필요시 클래스에 include

enumerable and comparable

  • 대표적인 mixin은 enumerable과 comparable
  • enumerable이 되기 위해선 each를 구현
  • comparable이 되기 위해선, <=>를 구현

enumerable

  • all? [{|obj| block}] -> true or false
  • any? [{|obj| block}] -> true or false
  • collect, map {|obj| block} -> array
  • find(ifnone = nil) {|obj| block} -> obj or nil
  • select, find_all{ |obj| block} -> array
  • inject(initial) {|memo, obj| block} -> obj

위와 같은 기본적인 enumerable의 함수가 제공된다. 다만, inject는 조금 특이한데, accumulator(initial이자 memo)을 사용합니다.

Self-Study : Find

  1. benefit of using code block or not
    File::open('test.txt', w) {|f| f << 'Added new line!'}
    
    f = File::open('test.txt', w)
    f << 'Added new line!'
    

    일견 비슷해 보이지만, code block을 사용하면 오류가 나더라도 close와 같은 resource 반환 작업을 신경쓸 필요가 없음

  2. converting between hash and array

    hs = {1 => 'one', :two => 2}
    hs.keys
    => [1, :two]
    hs.values
    => ["one", 2]
    
    

    hash to array

    a = [ "x", "y"]
    b = [ 12, 15]
    hs = Hash[a.zip(b)]
    

    array to hash

  3. To iterate hash

    hs.each_pair {|k, v| puts "#{k} - #{v}"}
    
  4. What data structure do arrays support?
    # queue
    arr.shift # take away the first     
    arr.slice!(0) # take away the first 
    
    # bag/set
    b = [1,2,3,3,4,4,4,5]   
    s = b.uniq
    insertsection = s & [4,5]
    => [4,5]
    
    # matrix
    matrix = [[1,2,3],[4,5,6],[7,8,9]]  
    matrix.transpose
    => [[1, 4, 7], [2, 5, 8], [3, 6, 9]]    
        

Self-Study : Exersize

  1. each vs each_slice
    (1..16).each do |x|
        r.push(x)
        if ( x % 4 == 0)
            p r
            r.clear
        end
    end 
    
    (1..16).each_slice(4) {|a| p a}
    
  2. To improve a class of Tree for a new constructor using Hash
    class Tree
      attr_accessor :children, :node_name
    
      def initialize(hash)
        p hash
        @node_name = hash.keys.first 
        @children = hash.values.first.map {|k,v| Tree.new(Hash[k,v]) unless k.empty?} 
      end
    
      def visit_all(&block)
        visit &block
        children.each {|c| c.visit_all &block} unless children.nil?
      end
    
      def visit(&block)
        block.call self
      end
    end
    
    class Tree
      def initialize(name,children=[])
        @node_name = name
        @children = children
      end
    end
    
  3. grep by Ruby
    reg = /block/   
    File::open('firstday.rb', 'r') {|f| f.each_with_index {|l,i| puts "#{i}-" + l if reg.match(l)}}
    

    정말, 간단하다. 작성하면서도, 이렇게 쉽게 할 수 있다는 사실에 놀랐다. 마츠가 가장 좋아한다는 block에 대한 장점을 잘 보여주는 코드이다. 익명 함수를 중첩적으로 사용한다는 점은 가독성에는 안좋을 수 있지만, celemony code를 피하기에는 너무 좋다.

The third day

method_missing

'귀가 막히고, 코가 막히는 특징'이었다. 이런게 하나쯤 있으면 당연히 좋겠다고 생각해 본 적이 있다.(진짜로 있을 줄이야...) 존재하지 않는 method를 실행하는 경우에 호출되는 함수이다. 당연히, override가 가능하고 이를 바탕으로 DSL(Domain Specific Language)을 만들기 좋다. 아래의 코드는 연습문제에 나오는 일부 코드이다.

class ColBasedRow
  attr_accessor :cols, :arr # Macro for property

  def initialize cols, arr
    @cols = cols
    @arr = arr
  end

  def method_missing name, *args # 문제의 함수, name은 Symbol!
    return @arr[@cols.index(name.to_s)] if @cols.include?(name.to_s)
    super
  end
end

module

method_missing이 재밌는 Tip같은 존재라면, module은 본격 Meta-Programming을 알리는 존재이다. 동적 메서드를 생성한다던가, 혹은 모듈 기반의 Mix-in에 대한 예제를 다루고 있다. 전자이던 후자인던, 다른 언어에서 사용하던 방식과는 많이 다른 느낌을 주었다. Mix-in에 대해서는, Javascript에서 Class 확장시에 사용되던 예제들만 보았었는데, 이렇게 보니 동적 언어의 위엄이 느껴졌다.

# ruby/acts_as_csv_module.rb
module ActsAsCsv
  def self.included(base)
    base.extend ClassMethods
  end
  module ClassMethods
    def acts_as_csv
      include InstanceMethods
    end
  end
  module InstanceMethods
    def read
      # body
    end

    attr_accessor :headers, :csv_contents

    def initialize
      read
    end
  end
end

class RubyCsv # mix in instead of inheritance
  include ActsAsCsv
  acts_as_csv
end

Self-Study : Exercise

문제는 예제 코드를 조금 더 확장하여, code block을 지원하는 each를 추가하고 column 이름으로 접근할 수 있도록 만들면 됩니다.

m = RubyCsv.new
m.each {|row| puts row.OffDay}

보나마나, method_missing을 적절히 사용하면 되겠습니다.

class ActsAsRow
  attr_accessor :arr

  def initialize columns, arr
    @arr = arr
    @cols = columns
  end

  def method_missing name, *args
    return @arr[@cols.index(name.to_s)] if @cols.include?(name.to_s)
    super
  end
end

class ActsAsCsv
  def read
    file = File.new(self.class.to_s.downcase + '.txt')
    @headers = file.gets.chomp.split(',')

    file.each do |row|
      @result << ActsAsRow.new(@headers, row.chomp.split(','))
    end
  end

  def each(&block)
    @result.each &block
  end

  def initialize
    @result = []
    read
  end
end

class RubyCsv < ActsAsCsv
end

위의 코드에서 보시는 것 처럼, row를 만들 때 새로운 class를 이용했습니다. 물론 array에 method_missing을 추가한다거나, 아니면 module에서 배운 것처럼, define_method를 수행해 보는 것도 좋을 것 같습니다.