스칼라 번역 - 명령형 프로그래밍(Imperative Programming)

이 포스팅은 [scala-exercises 번역 시리즈]scala-exercises 사이트의 스칼라 튜토리얼을 공부하며 번역한 문서 입니다.
scala-exercises 는 스칼라 창시자인 마틴 오더스키 강의의 강의자료입니다.
따라서 강의를 들으며 본 문서를 같이 보는것을 추천합니다.
의역이 많습니다. 오역 및 오타 등은 코멘트로 알려주세요 😄
원문 : [scala-exercises imperative programming]


지금까지의 예제에선 사이드 이펙트가 없었습니다.
시간이 진행되던 말던 어차피 같은 결과를 반환하기 때문에 시간이라는 개념은 중요하지 않았습니다.
종료되는 모든 프로그램에 대해 ‘일련의 모든 작업이 동일한 결과를 리턴’ 했습니다.
치환모델(substitution model)이기 때문입니다.

역자 추가)

1
2
3
4
5
6
7
8
sumOfSquares(3, 2+2)
sumOfSquares(3, 4)
square(3) + square(4)
3*3 + square(4)
9 + square(4)
9 + 4*4
9 + 16
25

‘일련의 모든 작업이 동일한 결과를 리턴’ 한다는 것은
sumOfSquares(3, 2+2) 에서 25가 구해지기까지의 과정들이 모두 동일한 결과(25)를 리턴한다는 뜻 입니다.
이렇게 계산식을 지속적으로 치환해나가며 결과를 얻어나가는 방식을 치환모델이라고 합니다.

substitution model(치환 모델)

재정의에 의해 다시 값이 구해집니다:

  • 이름은 방정식(정의)의 우측(right-hand side)으로 바꿔 평가됩니다.
  • 함수 어플리케이션은 함수의 우측으로 바꾸고 동시에 형식 파라미터(formal parameter)를 실제 인수(argument)로 바꾸어 구해집니다.

역자 추가)

x + 5 = y + 7

x + 5는 좌측(left-hand side, LHS) 이며 y + 7은 우측(right-hand side, RHS) 입니다.

iteratesquare 함수가 있다고 가정해봅시다:

1
2
3
def iterate(n: Int, f: Int => Int, x: Int) =
if (n == 0) x else iterate(n - 1, f, f(x))
def square(x: Int) = x * x

그런 다음, iterate(1, square, 3)호출은 다음과같이 재작성됩니다:

1
2
3
4
5
6
7
iterate(1, square, 3)
if (1 == 0) 3 else iterate(1 - 1, square, square(3))
iterate(0, square, square(3))
iterate(0, square, 3 * 3)
iterate(0, square, 9)
if (0 == 0) 9 else iterate(0 - 1, square, square(9))
9

재작성(rewriting)은 어떤 구간에서도 수행될 수 있으며,종료되는 모든 재작성은 같은 결과로 이어집니다.
이것은 함수형 프로그래밍의 이론인 람다-계산의 중요한 결과입니다.
예를들어, 이 두번의 재정의는 결국 같은 결과로 이어질 것입니다:

1
2
3
4
5
6
if (1 == 0) 3 else iterate(1 - 1, square, square(3))
iterate(0, square, square(3))

// OR
if (1 == 0) 3 else iterate(1 - 1, square, square(3))
if (1 == 0) 3 else iterate(1 - 1, square, 3 * 3)

Stateful Object

객체의 집합으로 일반적인 세상을 묘사하자면, 어떤건 시간에 따라 변하는 성질을 갖고있습니다(끓는 물 등) 이런 객체를 stateful 하다고 하며, 이 상태는 시간으로부터 영향을 받습니다.
예를들어 : 은행 계좌는 stateful 합니다. “100원을 인출할 수 있는가” 라는 질문은 계좌의 잔고상태에 따라 다르기 때문입니다.

Stateful Object 의 구현

변경가능한(mutable)상태인 객체는 변수로 구성됩니다.
변수 정의는 값 정의와 비슷하지만, val 대신 var 로 작성됩니다.

1
2
var x: String = "abc"
var count = 111

값 정의와 마찬가지로 변수 정의는 값을 이름과 연결합니다.
그러나, 이 변수 값의 경우 나중에 재할당을 통해 바뀔 수 있습니다:

1
2
x = "hi"
count = count + 1

객체안의 상태

실제로 상태가 있는 객체는 일반적으로 가변멤버가 있는 객체(var)로 표시됩니다.
다음은 은행 계좌 클래스입니다:

1
2
3
4
5
6
7
8
9
10
11
12
class BankAccount {
private var balance = 0
def deposit(amount: Int): Int = {
if (amount > 0) balance = balance + amount
balance
}
def withdraw(amount: Int): Int =
if (0 < amount && amount <= balance) {
balance = balance - amount
balance
} else throw new Error("insufficient funds")
}

BankAccount 클래스는 계좌 상태를 나타내는 balance 변수를 갖고 있습니다.
depositwithdraw 메소드는 balance의 할당에 의해 값이 변경됩니다.
BankAccount 클래스 안의 balance는 private이므로 외부 클래스에선 접근할 수 없습니다.
BanckAccount 객체는 다음과 같이 만들어집니다:

1
val account = new BankAccount

가변객체의 동작

다음은 계좌를 조작할 수 있는 프로그램입니다:

1
2
3
4
5
val account = new BankAccount // account: BankAccount = BankAccount
account deposit 50 //
account withdraw 20 // res1: Int = 30
account withdraw 20 // res2: Int = 10
account withdraw 15 // java.lang.Error: insufficient funds

같은 작업을 하는 코드가 있지만 각각 다른 결과를 리턴합니다.
명백하게, account는 상태 객체(stateful object)입니다.

IDENTITY와 변화

두 표현식이 “동일한”것인지를 결정하는 새로운 문제가 있습니다.
할당을 제외한 작성은 다음과 같습니다:

1
val x = E; val y = E

E는 임의의 표현식이며, x와 y는 같은 것으로 칩니다:

1
val x = E; val y = x

이 프로퍼티는 일반적으로 참조 투명하다라고 합니다.
그러나 할당을 허용하면, 두 공식은 달라집니다.
예를들어 다음과 같이 x, y 를 정의하면:

1
2
val x = new BankAccount
val y = new BankAccount

x 와 y는 같나요?

OPERATIONAL EQUIVALENCE

위의 x 와 y는 같냐는 질문에 답을 하려면 우선 “같음”의 의미에 대해 정의해야 됩니다.
“같아짐(being the same)”의 정확한 뜻은 operational equivalnance의 프로퍼티(property) 의해 정의됩니다.

  • 역자추가) 프로퍼티란’어떤 값’이며 이 값이 다른 값과 연관을 갖고있을때 프로퍼티 라고 부릅니다.
  • 예를들어 length 프로퍼티는 문자열 혹은 리스트등의 객체의 길이를 양의 정수로 나타낸 값을 갖고 있습니다.

다소 비공식적인 방식으로, 이 프로퍼티는 다음과 같이 명시됩니다:

  • x, y라는 두개의 정의가 있다고 가정합니다.
  • x 와 y는 구별할 수 있는 테스트가 없을 경우 연산적으로 동등합니다.

OPERATIONAL EQUIVALENCE 을 위한 테스팅

만약 x 와 y가 같음을 테스트한다면
가능한 결과를 관찰하며 x와 y를 포함하는 연산의 임의의 시퀀스 S에 따른 정의를 실행합니다.

1
2
3
val x = new BankAccount
val y = new BankAccount
f(x, y)

그런다음, 모든 y 발생 항목의 이름을 S의 x로 변경해서 얻은 다른 시퀀스인 S'로 정의를 실행합니다.

1
2
3
val x = new BankAccount
val y = new BankAccount
f(x, x)

만약 결과가 다르다면 x 와 y는 명백히 다른 표현식입니다.
반면에, 만약 (S, S') 시퀀스의 가능한 모든 쌍이 같은 결과를 나타낸다면, x 와 y는 같습니다.
이 정의에 따라 다음 표현식을 봅시다:

1
2
val x = new BankAccount
val y = new BankAccount

테스트 시퀀스별로 다음 정의를 봅시다:

1
2
3
4
val x = new BankAccount
val y = new BankAccount
x deposit 30
y withdraw 20 // java.lang.Error: insufficient funds

이제 이 시퀀스에서 y의 모든 항목의 이름을 x로 변경하면 다음의 결과를 얻게됩니다:

1
2
3
4
val x = new BankAccount
val y = new BankAccount
x deposit 30
x withdraw 20 shouldBe 10

두 예제의 최종 결과는 다릅니다. 결론적으로 x 와 y는 동일하지 않습니다.

OPERATIONAL EQUIVALENCE 의 성립

반면에, 만약 다음과 같이 정의한다면:

1
2
val x = new BankAccount
val y = x

x와 y 를 구분할 수 있는 일련의 작업이 없으므로 이 경우에 x와 y는 같습니다.

SUBSTITUTION MODEL 의 할당

위의 예는 치환에 의한 계산의 모델을 사용할 수 없음을 보여줍니다.
실제로 이 모델에 따르면, 항상 ‘값 이름을 정의하는 표현식’ 으로 값 이름을 바꿀 수 있습니다.
예를들어:

1
2
val x = new BankAccount
val y = x

y의 정의 안에 있는 x(val y = x)는 new BankAccount로 치환될 수 있습니다.
그러나 여기에 할당을 추가하게되면 치환모델은 불가능해집니다.
store를 도입하여 치환모델을 적용하는게 가능하긴 하지만 이럴경우 훨씬 더 복잡해집니다.

명령형 루프

첫번째 섹션에서 재귀를 사용해 루프를 작성하는 법을 살펴봤습니다.

While-Loop
그러나 재귀외에 while을 사용해 루프를 작성할 수 있습니다:

1
2
3
4
5
6
def power(x: Double, exp: Int): Double = {
var r = 1.0
var i = exp
while (i > 0) { r = r * x; i = i - 1 }
r
}

while의 조건이 true 상태인 동안 body는 계속해서 값이 구해집니다.

For-Loop
다음은 스칼라에서의 for 종류중 하나입니다:

1
for (i <- 1 until 3) { System.out.print(i + " ") }

이것은 1 2라는 결과를 보여줍니다.
for loop는 for 표현식과 비슷하지만 map과 flatMap 대신 foreach 콤비네이터(combinator)를 사용합니다.

  • 역자추가) 콤비네이터는 함수와 컬렉션 등 다른 식을 받아서 적절한 작업을 해주는 조합 장치(함수) 정도로 생각하면 됩니다.
  • 출처- 함수콤비네이터

타입 A 엘리먼트와 함께 forEach는 컬렉션으로 정의됩니다 :

1
2
def forEach(f: A => Unit): Unit =
// apply `f` to each element of the collection

예를들어 다음은:

1
for (i <- 1 until 3; j <- "abc") println(s"$i $j")

이렇게 바꿀 수 있습니다.

1
(1 until 3) foreach (i => "abc" foreach (j => println(s"$i $j")))

연습문제
factorial의 필수 구현을 완료하세요:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def factorial(n: Int): Int = {
var result = 1
var i = 1
while (i <= n) {
result = result * i
i = i + 1
}
result
}

factorial(2) shouldBe 2
factorial(3) shouldBe 6
factorial(4) shouldBe 24
factorial(5) shouldBe 120

읽어주셔서 감사합니다. 혹 글에 오역/추가할 내용이 있다면 코멘트 남겨주세요!

같이 보면 좋은 자료