데이터와 제어

함수와 제네릭

이름 있는 함수와 람다로 시작합니다. 필요할 때만 1계층 제네릭과 제약 있는 제네릭으로 넓힙니다.

함수는 반복해서 쓸 변환에 이름을 붙입니다. 그리고 경계를 오가는 값의 타입을 선언합니다. 먼저 구체적인 이름 있는 함수로 시작하세요. 짧은 동작을 값처럼 전달할 때는 람다를 쓰세요. 하나의 타입 관계를 여러 타입에서 지켜야 할 때만 타입 매개변수를 들입니다.

한 프로그램에서 단계별 형태 실행하기

다음 코드를 functions-generics.tpz로 저장합니다.

TOPAZ
record Label derives Show {
    text: string,
}

function applyTwice(value: int, step: (int) -> int) -> int {
    step(step(value))
}

function firstOr<T>(values: Array<T>, fallback: T) -> T {
    match values {
        case [] => fallback
        case [first, ..] => first
    }
}

function showValue<T: Show>(value: T) -> string {
    Show.show(value)
}

let increment: (int) -> int = value => value + 1
let first: string = firstOr(["Topaz", "Classic"], "none")
let shown = showValue(Label { text: "docs" })

print("{applyTwice(3, increment)}")
print(first)
print(shown)

실행하기 전에 검사합니다.

BASH
topaz check functions-generics.tpz
topaz run functions-generics.tpz

결과는 다음과 같습니다.

출력
5
Topaz
Label { text: docs }

이름 있는 함수부터 시작하기

applyTwice는 매개변수 두 개와 int 결과를 선언합니다. 함수 몸체의 마지막 표현식이 결과가 됩니다. 그래서 여기서는 return을 따로 적지 않았습니다. 중간에 일찍 빠져나가는 흐름을 분명히 보이고 싶을 때는 return을 쓰세요.

step의 타입은 (int) -> int입니다. 호출자는 int를 받아 int를 돌려주는 함수를 제공해야 합니다. increment가 바로 이 형태의 람다입니다. 람다는 =>로 씁니다. 다른 값처럼 저장할 수 있습니다. 그리고 클로저 규칙에 따라 주변 바인딩을 캡처합니다.

같은 관계가 반복될 때 타입 매개변수 추가하기

firstOr<T>는 배열 원소의 타입을 알 필요가 없습니다. 문자열이든 정수든 다른 타입이든 상관없습니다. 배열 원소와 대체 값, 결과가 모두 같은 타입 T라는 관계만 약속합니다. 이 호출은 문자열을 넘깁니다. 그래서 추론 결과가 T = string입니다. 호출자가 타입 인수를 직접 적지 않아도 됩니다.

이것이 1계층 다형성입니다. 이름 있는 함수가 제네릭입니다. 각 호출이 구체적인 타입 하나를 고릅니다. 같은 알고리즘이 여러 타입에서 실제 타입 관계를 지킬 때 쓰세요. 타입 고르기를 피하려고 함수를 제네릭으로 만들 필요는 없습니다.

함수 몸체가 연산을 요구할 때만 제약 추가하기

showValue<T: Show>Show.show를 호출합니다. 그래서 함수 몸체에는 T가 정적 Show 프로토콜을 따른다는 근거가 필요합니다. Label derives Show가 그 명목 타입에 필요한 준수를 붙여 줍니다. 출력할 수 있을 것처럼 보인다고 해서 구조적 값이나 기본 타입이 자동으로 프로토콜을 따르지는 않습니다.

제약은 평범한 제네릭을 쓴 다음 필요한 만큼만 덧붙이는 장치입니다. 함수 몸체가 두 프로토콜의 연산을 모두 쓴다면 제약을 묶어서 요구할 수 있습니다. 이름 있는 제네릭 함수에 T: Show + JSON처럼 적으면 됩니다. 이때 적는 순서는 상관없습니다.

가장 작은 함수 형태 고르기

필요한 것선택독자에게 전달되는 의미
안정적인 이름을 가진 재사용 동작구체적인 이름 있는 함수매개변수와 결과가 공개 계약을 이룹니다.
값으로 전달하거나 저장할 짧은 동작람다주변 문맥이 구체적인 매개변수 타입을 제공합니다.
여러 타입에서 하나의 관계를 보존하는 알고리즘제약 없는 이름 있는 제네릭같은 T가 입력과 결과를 연결합니다.
정적 프로토콜 연산을 호출하는 제네릭 몸체제약 있는 이름 있는 제네릭몸체가 사용할 수 있는 연산을 제약에 정확히 적습니다.

호출, 기본값, 가변 인수

매개변수는 불변입니다. 기본값은 인수를 생략할 때 계산합니다. 기본값에는 리터럴이나 const 표현식만 쓸 수 있습니다. 다른 매개변수를 참조할 수는 없습니다. 가변 인수 꼬리는 ...args: T로 씁니다. 이 꼬리는 반드시 마지막에 둡니다. 함수 안에서는 Array<T>로 받습니다.

호출할 때는 호출 대상을 먼저 한 번 계산합니다. 그다음 위치 인수와 스프레드 인수를 왼쪽에서 오른쪽으로 평가합니다. 이름 있는 인수는 그다음에 옵니다. 배열 스프레드는 가변 인수 꼬리에만 값을 보탤 수 있습니다. 아무 고정 매개변수나 채우는 용도로는 쓸 수 없습니다.

제네릭 타입은 보통 추론합니다. 타입 인수를 직접 적을 수 있는 호출 형태도 있습니다. 그때는 목록을 빠짐없이 적어야 합니다. 값 인수와 기대하는 결과의 타입에도 맞아야 합니다. 일부만 장식처럼 적으면 정적 오류입니다.

흔한 실수: 제네릭 람다부터 찾기

이름 없는 짧은 동작은 람다로 씁니다. 구체적인 매개변수 타입은 주변 문맥에서 받습니다. 타입 매개변수 선언과 프로토콜 제약은 이름 있는 함수 선언에만 둡니다. 동작 자체에 재사용할 제네릭 계약이 필요하다면 함수에 이름을 붙이세요. 그리고 그 계약을 한 번 선언하세요.

정확한 한계

  • 제네릭 함수는 1계층입니다. 고위 함수 값은 아직 지원하지 않습니다.
  • 람다, 함수 타입, 별칭, 명목 선언, 수신자 메서드, 프로토콜 메서드에는 제약을 선언할 수 없습니다.
  • 제네릭 람다, 타입 매개변수 기본값, 분산(variance) 표기, 동적 프로토콜 디스패치, 임의의 트레이트 제약은 아직 지원하지 않습니다.
  • 제네릭 함수 몸체는 선언한 제약만 가집니다. 더 강한 연산을 암묵적으로 쓸 수 없습니다.

첫 함수와 람다를 다시 보려면 값과 함수로 돌아가세요. 함수 타입과 제네릭 타입 형태는 타입, 캡처와 수명은 바인딩·스코프·클로저에서 이어집니다.