갈아 엎었다.
내 기준 그렇다.
전 코드에서는 if else
로 줄줄이 이어져 내려왔다.
하지만 보다보니 (다른 수강생분들이 하는 것과 비교해보니) ,
guard
문을 많이들 사용하더라. 보다보니 왜 쓰는지 알았다.
뭔가 조건이 만족하지 않으면 처리되는 부분이 한눈에 보기 편한 기분이랄까.
그래서 초반은 if let
을 사용해서 readLine()
의 반환 값을 안전하게 언래핑 해주고,
사용자가 입력한 값이 유효한지를 확인했다.
그 뒤로는 guard
를 사용하여 입력이 비어있지 않은지 추가로 체크!
이렇게 두 가지 방법을 조합하여 검증하는 건 안정성과 오류 처리를 향상시키는 좋은 방법이다.
~=
는 스위프트에서 범위를 체크해주는 연산자다.
왼쪽에 있는 범위에 오른쪽 값이 포함되는지를 확인해 주는데,
.
.
.
쉬운예시)
이런식으로 number
가 해당범위에 있는지 바로 확인해주니 편하다.
자 여기까지 하고 어제 하지 못한 힌트 클래스를 구현해보려 한다.
우선 calculateHints
라는 함수에 두개의 정수 배열
을 매개변수
로 먼저 받고,
반환 타입
으로는 (strike: Int, ball: Int)
라는 튜플
로 받았다.
튜플로 받으면 스트라이크와 볼 개수를 한번에 반환할 수 있기 때문이다.
한번에 반환한다는건.. 하나의 함수에서 여러 값을 동시에 반환한다거나
반환 된 값들을 쉽게 접근하고 분해해서 사용할 수 있는 거다.
저렇게 하면 나중에 실행할 때,
// 사용할 때
let calculator = Hintcalculator()
let result = calculator.calculateHints(answer: [1,2,3], userGuess:[1,3,2])
print(result.strike) // 이름으로도 접근이 가능하다.
print(result.ball)
이런식으로 스트라이크랑 볼이 항상 함께 계산되고 사용이 되기 때문에 튜플로 반환하는 것이 적절했다.
전체적으로 크게크게 묶어서 보자면,
zip
을 이용해 위에서 지정한 두 배열 (answer, userGuess)
을 순서대로 쌍으로 묶어줬다.
zip
은 저번 알고리듬 문제를 풀 때 배웠는데 쌍으로 순서대로 묶어주니 활용해봤다.
answer [1,2,3]
, userGuess [1,3,2]
라고 가정한다면,
-> [(1,1),(2,3),(3,2)]
이런식으로 묶어버린다.
그렇다면 zip
을 이용해 묶었으니,
map
을 이용해 각 쌍을 순회하면서 같은 위치의 숫자를 비교하게 된다면?
-> [(1,1), (2,3), (3,2)]
→ [1, 0, 0]
이렇게 원 스트라이크를 확인할 수 있는 형태가 된다.
그리고 삼항연산자
를 사용해서 만약 두 쌍이 같으면 1
, 다르면 0
을 반환하게 했다.
근데 스트라이크를 확인했다고 끝이 아니기 때문에,
return
값에 스트라이크 갯수를 더한 모든 값이 계산되어야한다.
그리고 이 방금까지 한 모든 것들은 map
의 클로저를 사용해
각 요소들을 어떻게 변환할지를 정의하는 것이라 보면 된다.
.reduce(0,+)
을 써서 배열의 모든 숫자를 더해주어야한다.
(0,+)
은 배열의 순서가 0에서부터 시작해서 모든 값을 더한다는 의미이다.
오늘 마침 아침에 푸는 알고리듬 문제 중에 짝수의 합
을 구하는 문제가 있었다.
그때 내가 reduce
라는걸 처음 이용했는데,
알고리듬을 풀면서 알게 된 메서드나 고차함수를 이용할 때마다 기분이 좋다.
첫 줄부터 보자면,
userGuess.filter {...}
부분은 사용자가 추측해서 적은 숫자 배열에서,
정답 배열에 포함된 숫자만 필터링 한다는 것이다.
처음에 난 스트라이크의 경우도 어쨋든 맞는 번호를 걸러내야하는 행위는 같으니까
스트라이크에도 filter
를 사용해야하는 것 아닌가? 라는 의문을 가졌었다.
하지만 확인해야하는 것에 대해 엄연히 다른 점이 존재했다.
스트라이크의 경우 :
볼의 경우 :
filter
로 값의 포함 여부만 확인해도 되는 것.결론은 만약 내가 의문을 가졌던 filter
로 스트라이크를 구현하게 된다면,
인덱스를 직관적인 면에서도 보기가 어렵고, 의도가 덜 명확하다는 결론이 났다.
목적에 따라 가장 알맞는 메서드를 이용하는 것, 그게 중요한 것 같다.
변역 해보면
"시퀀스에 주어진 요소가 포함되어 있는지 여부를 나타내는 부울 값을 반환합니다."
라고 한다.
여기서 보면 filter
다음에 클로저 문법이 사용되는 걸 볼 수 있다.
filter
메서드는 각 요소를 검사하는 과정에서 사용할 조건을,
클.로.저 로 받기 때문이다.
기억해야하는 맨날 잊는다...
아무튼 쉽게 본다면
.
.
.
filter
: 내가 걸러줄테니까 조건 좀 알려줘.
클로저
: 그래, 스트라이크에서 사용자가 추측한 숫자들 중에서
그 각각의 숫자를 gessedNumber
라고 부를게.
난 gessedNumber
가 정답 배열에 들어있는지 확인만 할거야
.
.
이런 느낌이다.
contains
는 정답(answer)배열 안에 guessedNumber
가 포함되는지 확인해주는 것.
그 배열내의 어떤 값과 일치하는지를 체크해주는 것이라고 보면된다.
그렇게 체크한 걸 .count
로 필터링 된 숫자들의 갯수를 세어준다. (숫자를 갯수화 시킴)
이 시점에서는 이제 스트라크와 볼이 모두 포함된 갯수인 것이다.
그래서 - strikeCount
를 해주어야 같은 숫자가 있지만 위치가 다른 경우만 남게 되는 것이다.
왜 근데 스트라이크 카운트를 빼줘야할까.
이유는 filter는 단순히 이 숫자가 정답 배열에 있는가. 만 검사해서 그렇다
answer = [1, 2, 3]
userGuess = [1, 3, 2]
// "사용자가 입력한 숫자들 중에서 정답 배열에 있는 모든 숫자"를 찾음
userGuess의 각 숫자를 검사해보면
1: 정답 배열에 있나? → 있음! (스트라이크)
3: 정답 배열에 있나? → 있음! (볼)
2: 정답 배열에 있나? → 있음! (볼)
// 이 시점에서 .count를 하면 3이 나옴
// 1(스트라이크), 3(볼), 2(볼) 모두 정답 배열에 있기 때문.
이러한 이슈로 스트라이크를 빼주어야 볼 갯수만 확인하여 사용자가 정답을 추측할 수 있게 된다.
정리하자면,
filter
후의 count
는 (스트라이크 + 볼)
의 개수가 됨strikeCount
를 빼면 진짜 볼의 개수만 남게 됨filter
이 메서드는.... 그냥 "이 숫자가 정답에 있나 없나" 만 체크하기 때문에,
스트라이크든 볼이든 상관없이 모든 매칭되는 숫자를 세게 된다.
그래서 나중에 스트라이크 개수를 빼줘야 실제 볼의 개수를 구할 수 있는 것.
그리고 마지막 return
을 보면 계산된 스트라이크의 볼의 개수를 튜플로 반환한다.
.
.
.
이유는,
위에서 함수의 반환타입을 (strike: Int, ball: Int)
로 정의해 뒀으니
이 함수는 두개의 정수를 포함하는 튜플을 반환해야 한다는 의미라서 그런 것이다.
그래서 함수내에서 return (strikeCount, ballCount)
처럼
튜플 형태로 묶어 반환한 것.
실행하고 숫자를 지정해준 뒤 프린트를 해보았다.
스트라이크와 볼의 갯수가 잘 나오는 걸 볼 수 있었다.
최종구현에서 보다시피 나는 여러 고차함수들을 사용했는데, 개인적인 평을 내려보자면
zip
, map
, reduce
, filter
등의 고차함수를 효과적으로 활용이 정도가 될 듯 하다.
사용자의 입력과 그리고 정답을 비교해서 스트라이크와 볼의 갯수를 계산하는 기능을 구현하는건
사실 지금 내 실력에서는 정말 까다롭고 어려운 수준이었다.
그래서 하나하나 공책에 정리를 해보았고, 그에 맞는 메서드와 고차함수를 이용하는 과정이 쉽진 않았다.
이번 구현 작업을 통해서 아무래도 고차함수를 좀 많이 사용했던터라
배열을 처리하는데 어떤 방법이 좀 더 효율적인 방법인지를 알게 되었고,
또한 튜플을 사용하는 이유가 여러 값을 한 번에 반환할 수 있기 때문이라는 것도 배웠는데
이렇게 하면 호출하는 쪽에서도 쉽게 구분이 가능하니 편하다는 장점 또한 알게 됐다.
이번 과제에서는 수강생들이 여러 메서드를 활용해볼 수 있게끔 하려는 좋은 의도가 보여서 힘들지만 재밌게 만들었다.
내일은 마지막 baseballGame
클래스를 만들어 볼 것이다.
저 이거 보고 ~= 연산자 따라 썼어요 잘했죠?