명시적 입/출력으로 변경하였다. 암묵적 입/출력 문제가 사라진 듯 하지만, 암묵적 출력 문제는 존재한다.
문제는 items 파라미터는 '참조' 로 전달이 되기 때문에, 함수 내 변경사항이 원본에 적용이 된다.
// items 파라미터는 참조로 전달된다, 즉 items 에 뭔가를 추가한다면,
// 파라미터로 전달된 원본도 같이 변경된다.
// return 값이 존재하지만, 암묵적 출력은 여전히 남아있다.
const addItemCalc = (items, name, price) => {
items.push({
name : name,
price : price
});
return items;
}
함수의 실행이, 함수 외부의 변수와 '완벽하게' 차단된다면 부수효과(side effect) 억제효과도 있고, 그만큼 테스트와 유지보수 용이성도 증가할 것이다.
해결방법 중 하나는 '복사본 리턴' 이다.
const addItemCalc = (items, name, price) => {
let result = items.slice(); //여기서 복사본을 만든다.
result.push({
name : name,
price : price
});
return result;
}
복사본을 만든 후, 배열을 변경 하고, 그 후에 변경된 배열을 리턴한다.
위와같이 구현하면, 큰 자료구조라 하더라도, 조작 시, 로직과 데이터를 완벽하게 분리 해낼 수 있다.
결론
동작을 계산으로 바꾸는 첫번째 방법 - 변경시에는 복사본을 사용하고 변경된 데이터를 리턴하라.
추가
- 객체 복사 시 성능 이슈가 있을 것이라 생각되는 분이 많을 듯 하다. 자바스크립트에서는 복사 시 기본적으로 얕은복사를 사용하고, 깊은 복사는 loadsh 등의 라이브러리를 사용하여 처리한다. 성능상의 이슈는 그리 크지 않은것으로 보이며, 필요할 경우에만 깊은 복사를 사용하면, 발생할 수 있는 성능상의 이슈를 줄일 수 있을 것이다.
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
아래 코드를 보자, 동작(Action) 이 있는 코드다.
let total = 0;
let item = [
{name : '새우깡', price: 100},
{name : '고래밥', price: 200},
{name : '꼬북칩', price: 300},
{name : '하이트맥주', price: 400},
];
const getTotalAmount = () => {
for(let i = 0; i < 4; i++) {
total += item[i].price;
}
console.log(`total: ${total}`)
};
getTotalAmount();
getTotalAmount 는 total에 item 배열의 price 값을 모두 더하는 간단한 함수.
위 코드에서 getTotalAmount 의 특징은
1. 암묵적인 입/출력이 있다.
- 암묵적 입력 : item
- 암묵적 출력 : total
2. 실행 횟수와 시점에 영향을 받는다.
- getTotalAmount 를 두번 호출한다면 item 배열의 요소를 두번 순회, total 값은 2000 이 된다.
때문에, getTotalAmount 는 '동작' 이고, getTotalAmount 의 유지보수 및 테스트를 위해서는 코드 전체를 확인한 후,
item 과 total 의 값 변경에 신경쓰며 작업해야한다.
작은 프로젝트에서야 저렇게 하는게 빠르고, 신경쓸 부분도 적기 때문에 상관이 없지만,
프로젝트가 증가하고, 같이 일할 사람들이 생길수록 저런 부분은 복잡성을 증가시키고, 여러 개발자들이 같은 작업을 계속하게된다.
(암묵적 입/출력 분석 및 상태 변경 처리 등...)
위 코드에서 암묵적 입/출력을 명시적 입/출력으로만 변경하여도, 유지보수 시 작업의 복잡도를 줄일 수 있다.
//getTotalAmountCalc 의 계산버전
const getTotalAmountCalc = (itemList) => {
let result = 0;
for(let i = 0; i < 4; i++) {
result += itemList[i].price;
}
return result;
}
getTotalAmountCalc 함수가 갖는 특징
1. 명시적인 입/출력이 생김
- 입력: itemList
- 출력: result
2. 실행 횟수와 시점의 영향을 받지 않음
- 100 번을 실행해도 itemList 가 같으면 항상 같은 값이 출력됨
위 1,2 번의 특징으로 인해 갖는 장점
1. 코드 테스트 부하가 상대적으로 감소
- 입력값에만 영향을 받으므로, 개발자가 원하는 특정 상황에서 테스트 가능
2. 유지보수 시 공수 감소
- 문서에는 '입력' 과 '출력' 타입 및 동작만 기술하면, 이후 개발자는 입출력만 신경써서 개발하면 된다.
(정 못믿겠으면, 개발 전에 테스트용 입력값 갖고 테스트만 하면됨.)
위 1,2 번의 특징으로 인해 갖는 단점
1. 코드량 증가
- 함수 첫부분 선언코드, 마지막의 return 코드
2. 메모리 추가사용
- 함수 안의 지역변수 선언, 지금은 작은 사이즈라 상관없지만, 크기가 자꾸 커지면???
단점이 없지는 않지만, 향후 정리할 '얕은 복사' 와 '방어적 복사' 등을 사용하고,
요즘 GC 들은 많은 부분에서 최적화가 이루어지고 있기 때문에, 기가바이트 급의 대용량이 아니라면,
저 단점들은 '투자' 의 개념으로 생각하는게 좋지 않을까 한다.
결론
동작을 계산으로 바꾸는 첫번째 방법 - 입/출력을 명시적으로 선언하라
=> 비지역 변수 read 는 함수의 파라미터로, 비지역변수 write 는 함수의 리턴값으로 바꿀 것.
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
Akka 코딩 공작소 예제중에
ScalaTest 3.2.0 과는 다른 부분이 있어 기록해둠.
원래 코드는 아래와 같다.
import org.scalatest.{WordSpecLike, MustMatchers}
import akka.testkit.TestKit
import akka.actor._
//This test is ignored in the BookBuild, it's added to the defaultExcludedNames
class SilentActor01Test extends TestKit(ActorSystem("testsystem"))
with WordSpecLike
with MustMatchers
with StopSystemAfterAll {
// Commented to make the travis build pass, this is the original test in the book
"A Silent Actor" must {
"change state when it receives a message, single threaded" in {
//Write the test, first fail
fail("not implemented yet")
}
"change state when it receives a message, multi-threaded" in {
//Write the test, first fail
fail("not implemented yet")
}
}
}
ScalaTest 3.2.0 에서는 객체들 이름이 변경되었다, 자세한 내용은 아래 링크를 참고바란다.
WordSpecLike -> AnyWordSpecLike 로 변경 (org.scalatest.wordspec.AnyWordSpecLike 패키지 사용)
MustMatchers -> Matchers 로 변경 (org.scalatest.matchers.must.Matchers 패키지 사용)
두가지다, 이부분을 적용해보면,
class SilentActor01Test extends TestKit(ActorSystem("testsystem"))
with AnyWordSpecLike /*scalatest 3.2.0 에서 변경*/
with Matchers /*scalatest 3.2.0 에서 변경*/
with StopSystemAfterAll {
"A Silent Actor" must {
"change state when it receives a message, single threaded " in {
//Write the test, first fail
fail("not implemented yet")
}
"change state when it receives a message, multi-threaded" in {
//Write the test, first fail
fail("not implemented yet")
}
}
}
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
코세라 강의 보는 중에
A <: B 이고 C[A] <: C[B] 인걸 Covariant
A <: B 이고 C[A] >: C[B] 인걸 Contravariant 라고 한다.
Covariant 는 이미 알고있는 거고,
Contravariant 가 무슨소린지 도저히 모르겠어서, 구글링... 하다가 하나 발견... 4시간에 걸쳐 겨우 이해하고, 정리글을 남긴다.
아래와 같이 클래스들이 있고, 함수 타입 한개를 선언한다.
그리고 몇가지 함수들을 선언한다.
class Animal {}
class Bird extends Animal {}
class Chicken extends Bird {}
class Duck extends Bird {}
object HelloWorld {
type FunctorType[A] = (A => String) //함수타입 한개 선언
val birdFunc : FunctorType[Bird] = { a => "Bird" }
val animalFunc : FunctorType[Animal] = { a => "Animal" }
val chickenFunc : FunctorType[Chicken] = { a => "Chicken"}
val duckFunc : FunctorType[Duck] = { a => "Duck"}
}
이경우 클래스 관계는
Animal <- Bird <- Chicken
Animal <- Bird <- Duck
이런 형식으로 된다. 다들 알다시피
하지만 함수 아래와 같이 함수가 파라미터로 전달 될 때도, 다형성이 그대로 유지될까?
def test_contravariant(a: FunctorType[Bird]): Unit = {
val ret1 = a(new Chicken);
println(ret1)
val ret2 = a(new Bird);
println(ret2)
}
class Animal extends LiveObject {
override def introduce(): Unit = {
println("I am Animal")
}
}
class Human extends LiveObject {
override def introduce(): Unit = {
println("I am Human")
}
}
간단하다 introduce 를 재정의 해서 문자열 하나만 다르게 재정의 했다.
준비작업은 모두 끝났다. 이제 main 함수 내에서 testFunction 을 호출을... 했는데? 어라? 에러가 있네?
에러 내용을 자세히 보면
직접 넣어줄 수는 있다.. 근데 이건 '암시적' 아닌 것 같아 다른 방법을 찾아본다.
바로 위에 Animal 변수를 하나 추가해준다.. 이러면 돌아가겠지.
object HelloWorld {
def main(args: Array[String]): Unit = {
println("hello, world!")
val animal = new Animal
testFunction("thomas")
}
def testFunction(name : String)(implicit liveObject : LiveObject) : Unit = {
println("I am " + name)
liveObject.introduce()
}
}
여전히 에러가 난다.
암시적 파라미터 관련 스칼라 문서를 다시 읽어보면...
스칼라 컴파일러는 implicit 파라미터를 만났을 때, 실제 파라미터에 할당할 변수를 검색하는데 규칙은 다음과 같다.
1. 메서드가 호출되었을 때, prefix 없이 접근할 수 있는 변수 (당연히 변수들은 implicit 키워드가 붙어야 함.)
2. implicit 키워드가 붙은 암시적 파라미터와 관련된 모든 멤버
2번은 조금 어려우니 일단 1번부터, 암시적 파라미터를 설정하려면 implicit 을 붙여야 한다. 변수를 implicit 으로 변경해서 다시.
object HelloWorld {
def main(args: Array[String]): Unit = {
println("hello, world!")
implicit val animal = new Animal
testFunction("thomas")
}
def testFunction(name : String)(implicit liveObject : LiveObject) : Unit = {
println("I am " + name)
liveObject.introduce()
}
}
이번엔 실행이 제대로 된다.
hello, world!
I am thomas
I am Animal
Process finished with exit code 0
암시적 변수 / 함수를 선언하려면 implicit 키워드를 사용하여 선언해야 정상적으로 인식하는듯
그러면 이번에 같은 type 을 두 개 넣으면 어떻게 될까?
def main(args: Array[String]): Unit = {
println("hello, world!")
implicit val animal = new Animal
implicit val human = new Human
testFunction("thomas")
}
실행 해보니 아래와 같은 Exception 이 발생한다.
머릿속에 어느정도 그림이 그려진다.
implicit 파라미터는 "함수가 호출 된 타이밍" 에 prefix 없이 접근 가능한 변수 중 "가장 가까운 변수 " 가 할당되다 라고 이해하면 빠를듯.
다음번엔 implicit function 을 좀 더 봐야겠다.
2019년 11월 18일 추가
변수의 scope 가 다를경우 같은 타입의 implicit 의 선언이 가능하다. 이 경우에 가장 최근에 선언된 implicit 변수가 사용된다.
object HelloWorld {
def main(args: Array[String]): Unit = {
println("hello, world!")
implicit val human = new Human
overlapfunction()
}
def overlapfunction()(implicit liveObject : LiveObject) : Unit = {
implicit val animal = new Animal
testFunction("thomas")
}
def testFunction(name : String)(implicit liveObject : LiveObject) : Unit = {
println("I am " + name)
liveObject.introduce()
}
}