중첩된 레코드를 갱신할 때는 렌즈를 사용해요(불변 데이터를 다루는 언어에 해당해요).
중첩된 불변 레코드의 필드를 갱신하는 일은 꽤 번거로워요. 그런 경우의 코드는 구조가 깊은 만큼이나 장황해져요. 예를 들어 Person이라는 레코드가 있고, 그 안에 Address가, Address 안에 Street가, Street 안에 Number가 있다고 해봐요. Number를 갱신하려면 새 Number를 담은 새 Street를 만들고, 그 새 Street를 담은 새 Address를 만들고, 마지막으로 그 새 Address를 담은 새 Person을 만들어야 해요. 벌써부터 헷갈리죠?
이 문제를 해결하는 한 가지 방법은 렌즈를 사용하는 거예요.
렌즈를 사용해 레코드에 접근하는 함수 몇 개를 구현해 봐요. 테스트 스위트는 렌즈를 아예 쓰지 않고도 여러 가지 접근 방식을 실험해 볼 수 있도록 허용해요.
Roc에서는 렌즈를 그렇게 많이 쓰지는 않아요. 레코드 업데이트 문법이 대부분의 사용 사례를 잘 처리해 주기 때문이에요. 예를 들어, 이런 중첩 레코드를 살펴봐요:
bob = { hair: { color: Brown, length: Long }, mouth: Smiling, height: 6.0 }
이제 Bob의 머리를 잘라볼까요:
new_bob = { ..bob, hair: { ..bob.hair, length: Short } }
렌즈가 필요 없어요! 그렇지만 Roc에서 렌즈가 드문 것일 뿐, Haskell이나 Scala 같은 다른 언어에서는 흔히 쓰이니까, Roc에서는 렌즈를 어떻게 구현할 수 있을지 궁금할 수도 있어요. 매개변수화된 타입이나 함수를 담은 레코드처럼 Roc의 고급 기능을 연습하기에도 좋은 방법이에요. 실제로 Roc에서 렌즈를 표현하는 한 가지 방법은 이래요:
Lens(whole, part) := {
get : whole -> part,
set : whole, part -> whole,
}
이것은 매개변수화된 타입이에요. whole과 part는 어떤 타입이든 될 수 있어요. whole은 보통 레코드이고, part는 어떤 필드 타입이든 될 수 있어요. get 필드에는 주어진 whole의 특정 part를 반환하는 함수가 들어 있고, set 필드에는 주어진 whole의 복사본을 반환하는 함수가 들어 있어요. 다만 특정 part가 주어진 part로 바뀐 상태예요.
예를 들어, 사람의 머리카락을 가져오거나 설정하는 렌즈는 이래요:
hair_lens = {
get: |person| person.hair,
set: |person, hair| { ..person, hair },
}
이 렌즈를 써서 Bob의 머리카락에 초점을 맞출 수 있어요. 이렇게요:
bob_hair = bob |> hair_lens.get
머리카락을 바꾸는 건 이렇게 해요:
new_hair = { ..bob.hair, length: Short }
new_bob = bob |> (hair_lens.set)(new_hair)
참고: hair_lens.set을 감싼 괄호는 꼭 필요해요. 괄호가 없으면 Roc는 hair_lens의 타입 안에서 set이라는 이름의 함수를 찾으려고 할 거예요.
렌즈는 서로 합성하기 시작하면 특히 유용해져요. 깊이 중첩된 부분을 가져오고 설정할 수 있게 해주거든요. 이건 다음과 같은 함수를 구현하면 돼요:
compose : Lens(outer, inner), Lens(inner, part) -> Lens(outer, part)
이 함수를 구현하고 나면, 이렇게 머리카락 길이 렌즈를 만드는 데 쓸 수 있어요:
length_lens = {
get: |hair| hair.length,
set: |hair, length| { ..hair, length },
}
hair_length_lens = compose(hair_lens, length_lens)
이제 Bob의 머리를 한 번 더 잘라줄 수 있어요:
set_hair_length = hair_length_lens.set
new_bob = bob |> set_hair_length(Short)
중첩된 부분에 어떤 변환이든 적용할 수 있게 해주는 함수를 구현하고 싶을 수도 있어요:
over : Lens(whole, part), whole, (part -> part) -> whole
구현하고 나면 over를 이렇게 쓸 수 있어요:
cut_hair = |current_length| match current_length {
Long => Short
Short => Bald
Bald => Bald
}
new_bob = hair_length_lens |> over(bob, cut_hair)
이 연습 문제에서 테스트는 코드가 다음 중첩 레코드의 여러 중첩된 부분을 잘 수정할 수 있는지 확인해요:
test_person : Person
test_person = {
name: { forenames: "Jane Joanna", surname: "Doe" },
birth: {
born_at: { street: "Longway", house_number: 1024, place: "Springfield", country: "United States" },
born_on: { year: 1984, month: 4, day: 12 },
},
address: { street: "Shortlane", house_number: 2, place: "Fallmeadow", country: "Canada" },
}
먼저 Roc의 레코드 업데이트 문법으로 테스트를 통과시켜 보고, 그다음에 렌즈를 써보는 걸 추천해요.
Exercism에 가입하고 Roc 트랙을 연습 문제 125개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.