3. 기본문법3(Containers, Blocks, and Iterators)

IT(Old)/RubyOnRails 2008. 1. 15. 16:01

1. Containers

DATA를 저장하는 Container객체에 대해 알아보자
이런저런 설명 빼고 어떻게 사용하는지만 보면 될 듯 싶다.

- Arrays

a = [ 3.14159, "pie", 99 ] 
a.type  » Array 
a.length  » 3 
a[0]  » 3.14159 
a[1]  » "pie" 
a[2]  » 99 
a[3]  » nil 
 
b = Array.new 
b.type  » Array 
b.length  » 0 
b[0] = "second" 
b[1] = "array" 
b  » ["second", "array"] 


이건 그냥 대략 이해 가고

a = [ 1, 3, 5, 7, 9 ] 
a[-1]  » 9 
a[-2]  » 7 
a[-99]  » nil


여기서 주의할 점은 -(마이너스)인덱스도 거꾸로 1까지만 가고 그 이상이 되면(-6) nil값을 return한다.

a = [ 1, 3, 5, 7, 9 ] 
a[1, 3]  » [3, 5, 7] 
a[3, 1]  » [7] 
a[-3, 2]  » [5, 7]
 

이것은 arrya[start, count] 방식으로 사용한다.

a = [ 1, 3, 5, 7, 9 ] 
a[1..3]  » [3, 5, 7] 
a[1...3]  » [3, 5] 
a[3..3]  » [7] 
a[-3..-1]  » [5, 7, 9]
 

.. 은 마지막것을 포함 ...은 마지막을 제외한다.

a = [ 1, 3, 5, 7, 9 ] » [1, 3, 5, 7, 9]
a[1] = 'bat' » [1, "bat", 5, 7, 9]
a[-3] = 'cat' » [1, "bat", "cat", 7, 9]
a[3] = [ 9, 8 ] » [1, "bat", "cat", [9, 8], 9]
a[6] = 99 » [1, "bat", "cat", [9, 8], 9, nil, 99]


마지막에 인덱스를 건더뛰게되면 중간에 빈것은 nil

a = [ 1, 3, 5, 7, 9 ] » [1, 3, 5, 7, 9]
a[2, 2] = 'cat' » [1, 3, "cat", 9]
a[2, 0] = 'dog' » [1, 3, "dog", "cat", 9]
a[1, 1] = [ 9, 8, 7 ] » [1, 9, 8, 7, "dog", "cat", 9]
a[0..3] = [] » ["dog", "cat", 9]
a[5] = 99 » ["dog", "cat", 9, nil, nil, 99]


여기서는 처음것은 index 두번째것은 length(0은 빈곳 삽입, 1은 한개만 replace, 2는 두개 replace)

- Hashes

h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' } 
 
h.length  » 3 
h['dog']  » "canine" 
h['cow'] = 'bovine' 
h[12]    = 'dodecine' 
h['cat'] = 99 
h  » {"cow"=>"bovine", "cat"=>99, 12=>"dodecine", "donkey"=>"asinine", "dog"=>"canine"}
 

2. Implementing a SongList Container

class Song
 
  attr_reader :name, :artist, :duration
  def initialize(name, artist, duration)
    @name = name
    @artist = artist
    @duration = duration
  end
  def name
    @name
  end
  def artist
    @artist
  end
  def duration
    @duration
  end
  def duration=(newDuration)
    @duration = newDuration
  end
  def durationInMinutes
     @duration/60.0
   end
end

class SongList
  def initialize
    @songs = Array.new
  end
 
  def append(aSong)
    @songs.push(aSong)
    self
  end
 
  def deleteFirst
    @songs.shift
  end
  def deleteLast
    @songs.pop
  end
 
  def [] (key)
    if key.kind_of?(Integer)
      @songs[key]
    else
      # ...
    end
  end
end

list = SongList.new
list.
  append(Song.new('title1','artist1',1)).
  append(Song.new('title2','artist2',2)).
  append(Song.new('title3','artist3',3)).
  append(Song.new('title4','artist4',4))
 
 
#puts list.deleteFirst
#puts list.deleteFirst
#puts list.deleteFirst
#puts list.deleteFirst
#puts list.deleteFirst

#puts list[0]
#puts list[2]
#puts list[9]


여기서 shift 는 queue라 봤을때 처음값을 삭제
pop은 마지막것을 삭제한다 보면 된다.

key.kind_of?(Integer) 부분은 인자값을로 받은것이 정수인지 구분

3. Blocks and Iterators

위의것은 인덱스로만 검색하는 것이고 이제부터는 문자열로 검색 가능토록 만들어보자

총 3가지 방법이 있다.
첫번째는

class SongList
  def [](key)
    if key.kind_of?(Integer)
      return @songs[key]
    else
      for i in
0...@songs.length
        return @songs[i] if key == @songs[i].name
      end
    end
    return nil
  end
end


다른 언어에서와 같이 평범하다. 여기에 루비 문법을 적용해보면
두번째와 같다.

class SongList
  def [](key)
    if key.kind_of?(Integer)
      result = @songs[key]
    else
      result = @songs.find { |aSong| key == aSong.name }
    end
    return result
  end
end


어랏 간단해졌네... 하지만 이것도 맛배기.. 더 간단히 줄여보자
세번째는 다음과 같다.

class SongList
  def [](key)
    return @songs[key] if key.kind_of?(Integer)
    return @songs.find { |aSong| aSong.name == key }
  end
end


헐;;; 사람들이 루비 칭찬하는 이유를 알겠다.....
이제부터 나는 세번째 방법으로 할련다.

- Implementing Iterator

앞서 아래와 같이 yield의 간단한 사용법을 배웠다
def threeTimes
  yield
  yield
  yield
end
threeTimes { puts "Hello" }

 
produces: Hello
Hello
Hello


이것 응용한 것을 보면 다음과 같다.(아 머리 아파 하지 말자)

def fibUpTo(max)
  i1, i2 = 1, 1        # parallel assignment
  while i1 <= max
    yield i1
    i1, i2 = i2, i1+i2
  end
end
fibUpTo(1000) { |f| print f, " " }

produces: 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

값의 흐름을 보면 간단하니깐
i1  i2
1    1
1    2
2    3
3    5
5    8
....
610 987
이런 형태로 값이 쌓인다.

주의해서 볼 것은 병렬로 값의 저장이 가능하다는 것과
yield로 자기를 호출한 것에 다시 값을 return 해 준다는 것.. 쫌만 쬐려보면 금방 이해 된다.

더 응용된 예제를 보자

class Array 
  def find 
    for i in 0...size 
      value = self[i] 
      return value if yield(value) 
    end 
    return nil 
  end 
end
 
 
[1, 3, 5, 7, 9].find {|v| v*v > 30 } 

위 예제는 find를 하면서 loop를 돌고
일방적으로 값을 넘기는 것이 아니고 서로 통신하고 잇따 보면 된다. 결과 값을 puts로 찍으면 7
설명이 길어지니 pass

또다른 형태의 iterator 방법을 보자

[ 1, 3, 5 ].each { |i| puts i }
 
produces: 1
3
5


각각의 값을 받고 1, 3, 5출력

puts ["H", "A", "L"].collect { |x| x.succ }

이것은 I B M 출력한다.
succ는 바로 다음 ascii코드를 뿌리는건가? 에랏 모르겠다.

4. Ruby Compared with C++ and Java

class Array 
  def inject(n) 
     each { |value| n = yield(n, value) } 
     n 
  end 
  def sum 
    inject(0) { |n, value| n + value } 
  end 
  def product 
    inject(1) { |n, value| n * value } 
  end 
end 
puts [ 1, 2, 3, 4, 5 ].sum  #≫ 15 
puts [ 1, 2, 3, 4, 5 ].product  #≫ 120


이것도 설명이 길어지므로 pass

5. Blocks for Transactions

Blocks can be used to define a chunk of code that must be run under some kind of transactional control. For example, you'll often open a file, do something with its contents, and then want to ensure that the file is closed when you finish. Although you can do this using conventional code, there's an argument for making the file responsible for closing itself. We can do this with blocks. A naive implementation (ignoring error handling) might look something like the following.

class File
  def File.openAndProcess(*args)
    f = File.open(*args)
    yield f
    f.close()
  end
end

File.openAndProcess("testfile", "r") do |aFile|
  print while aFile.gets
end

 
produces: This is line one
This is line two
This is line three
And so on...



This small example illustrates a number of techniques. The openAndProcess method is a class method---it may be called independent of any particular File object. We want it to take the same arguments as the conventional File.open method, but we don't really care what those arguments are. Instead, we specified the arguments as *args, meaning ``collect the actual parameters passed to the method into an array.'' We then call File.open, passing it *args as a parameter. This expands the array back into individual parameters. The net result is that openAndProcess transparently passes whatever parameters it received to File.open .

Once the file has been opened, openAndProcess calls yield, passing the open file object to the block. When the block returns, the file is closed. In this way, the responsibility for closing an open file has been passed from the user of file objects back to the files themselves.

Finally, this example uses do...end to define a block. The only difference between this notation and using braces to define blocks is precedence: do...end binds lower than ``{...}''. We discuss the impact of this on page 234.

The technique of having files manage their own lifecycle is so useful that the class File supplied with Ruby supports it directly. If File.open has an associated block, then that block will be invoked with a file object, and the file will be closed when the block terminates. This is interesting, as it means that File.open has two different behaviors: when called with a block, it executes the block and closes the file. When called without a block, it returns the file object. This is made possible by the method Kernel::block_given? , which returns true if a block is associated with the current method. Using it, you could implement File.open (again, ignoring error handling) using something like the following.

class File
  def File.myOpen(*args)
    aFile = File.new(*args)
    # If there's a block, pass in the file and close
    # the file when it returns
    if block_given?
      yield aFile
      aFile.close
      aFile = nil
    end
    return aFile
  end
end


6. Blocks Can Be Closures

Let's get back to our jukebox for a moment (remember the jukebox?). At some point we'll be working on the code that handles the user interface---the buttons that people press to select songs and control the jukebox. We'll need to associate actions with those buttons: press STOP and the music stops. It turns out that Ruby's blocks are a convenient way to do this. Let's start out by assuming that the people who made the hardware implemented a Ruby extension that gives us a basic button class. (We talk about extending Ruby beginning on page 169.)

bStart = Button.new("Start")
bPause = Button.new("Pause")

# ...

What happens when the user presses one of our buttons? In the Button class, the hardware folks rigged things so that a callback method, buttonPressed, will be invoked. The obvious way of adding functionality to these buttons is to create subclasses of Button and have each subclass implement its own buttonPressed method.

class StartButton < Button
  def initialize
    super("Start")       # invoke Button's initialize
  end
  def buttonPressed
    # do start actions...
  end
end

bStart = StartButton.new

There are two problems here. First, this will lead to a large number of subclasses. If the interface to Button changes, this could involve us in a lot of maintenance. Second, the actions performed when a button is pressed are expressed at the wrong level; they are not a feature of the button, but are a feature of the jukebox that uses the buttons. We can fix both of these problems using blocks.

class JukeboxButton < Button
  def initialize(label, &action)
    super(label)
    @action = action
  end
  def buttonPressed
    @action.call(self)
  end
end

bStart = JukeboxButton.new("Start") { songList.start }
bPause = JukeboxButton.new("Pause") { songList.pause }

The key to all this is the second parameter to JukeboxButton#initialize. If the last parameter in a method definition is prefixed with an ampersand (such as &action), Ruby looks for a code block whenever that method is called. That code block is converted to an object of class Proc and assigned to the parameter. You can then treat the parameter as any other variable. In our example, we assigned it to the instance variable @action. When the callback method buttonPressed is invoked, we use the Proc#call method on that object to invoke the block.

So what exactly do we have when we create a Proc object? The interesting thing is that it's more than just a chunk of code. Associated with a block (and hence a Proc object) is all the context in which the block was defined: the value of self, and the methods, variables, and constants in scope. Part of the magic of Ruby is that the block can still use all this original scope information even if the environment in which it was defined would otherwise have disappeared. In other languages, this facility is called a closure.

Let's look at a contrived example. This example uses the method proc, which converts a block to a Proc object.

def nTimes(aThing) 
  return proc { |n| aThing * n } 
end 
 
p1 = nTimes(23) 
p1.call(3)  » 69 
p1.call(4)  » 92 
p2 = nTimes("Hello ") 
p2.call(3)  » "Hello Hello Hello "
 

The method nTimes returns a Proc object that references the method's parameter, aThing. Even though that parameter is out of scope by the time the block is called, the parameter remains accessible to the block