매개변수 문법 제안

, by disjukr

📝 이 글은 정보를 전달하는 글은 아니고, 이렇게 프로그래밍 언어를 디자인 해보면 어떨까 구상하는 글입니다.

저는 JavaScript가 익숙하기 때문에 이미 존재하는 언어의 예시를 들 때는 JavaScript(또는 TypeScript)를 사용하겠습니다.
구상한 문법을 작성할 때는 JavaScript와 확실히 구분되도록 가상의 문법을 사용하겠습니다.

기존 C 계열의 프로그래밍 언어들에서는 함수를 선언할 때 매개변수가 어떻게 생겼는지 함수 이름 옆에 같이 기술합니다.

function addN(input) {
  //          ^ 함수 이름 옆에 매개변수가 위치합니다.
  let n = 1;
  return input + n;
}

addN(1); // 2

매개변수는 다른 변수들과 마찬가지로 함수 스코프 안에서 사용할 수 있는 변수입니다.
다른 변수들과의 차이는 초기값을 주입하는 위치가 함수를 호출하는 곳이라는 것 뿐입니다.

그렇다면 매개변수를 선언하는 위치는 다른 변수들과 같아도 상관없지 않을까요?

fn add_n { // 기존 매개변수 문법 생략
  let input = ?; // 변수 선언 및 초기화 문법과 비슷한 모양으로 매개변수 선언
  let n = 1;
  return input + n;
}

add_n(1) // 2

그리고, 변수 n을 기본값이 들어간 매개변수라고 본다면 어떨까요?

add_n(1, n=2); // 3

짧게 말하면 이 제안은 함수 안의 모든 변수 선언문을 매개변수 선언으로 취급해보자는 것입니다.

이유

첫번째

제가 코딩을 하면서 거의 항상 만나는 상황이 있습니다.

function foo(input) {
  let input2 = bar(input);
  // ...이후로 `input2`만 활용함
}

foo를 호출할 때 넣어주고 싶은 인자의 모양(input)과, foo 안에서 다루고 싶은 인자의 모양(input2)이 다를 때 이렇게 됩니다.

그런데 리팩토링, 최적화 등의 이유로 저 패턴을 걷어낸 함수가 필요해지는 경우가 생깁니다.

function foo(input) {
  let input2 = bar(input);
  foo2(input2);
}

function foo2(input) {
  // ...원래 foo에 들어있던 내용
}

이런 때에 로직을 별도 함수로 나누면서 작명의 고통을 겪고 싶지가 않습니다.

다른 변수를 선택해서 인자를 주입할 수 있으면 별도의 함수를 나누지 않고 문제를 해결할 수 있을 겁니다.

fn foo {
  let input = ?;
  let input2 = bar(input);
  // ...
}

foo(easy_to_use);
foo(input2=tricky_but_cheaper_approach());

두번째

TypeScript를 사용하다 보면, 특정 함수에서만 사용되는 유틸 타입을 함수 시그니처 위치에서 사용해야하기 때문에, 함수 바깥에 유틸 타입을 선언해야 하는 상황이 종종 생깁니다.

type Outer<T> = blabla;
function foo<T>(a: Outer<T> /* 이 곳에서는 `Inner<T>` 타입 사용 불가 */) {
  type Inner<T> = blabla;
}

매개변수가 선언되는 위치를 함수의 다른 변수와 동일하게 하면, 자연스럽게 지역 타입 선언을 공유할 수 있다는 장점이 있습니다.

fn foo<T> {
  type Inner<T> = blabla;
  let a: Inner<T> = ?;
}

예상되는 문제점과 해결 방안

이렇게 매개변수 문법을 디자인하면 코드를 읽고 함수 시그니처(입출력 형식)를 알기 어려운 점, 함수 동작을 약간만 수정해도 함수 시그니처가 쉽게 바뀔 수 있는 점이 문제가 될 수 있을 것 같습니다.

함수 사용자가 함수호출시 인자를 어떻게 집어넣느냐에 따라서 함수 작성자가 의도한 것과는 완전히 다른 동작을 할 수도 있겠습니다.

이런 문제는 함수 시그니처를 별도로 명시할 수 있는 문법을 만들고, 모듈 단위에서는 무조건 바깥에서 바라볼 수 있는 인터페이스를 명시하도록 해서 완화할 수 있을 것 같습니다.

좀 더 구체적인 동작 예시

fn foo {
  let a = ?;
  let b = bar(a);
  let c = a;
  let d = b;
  print(c); // <- 여기
}

fn bar {
  let input = ?;
  print(input); // <- 저기
  return input;
}

fn baz {
  if true {
    let a = "Hello";
    print(a);
  }
}
  • foo() - (오류) a 인자는 기본값이 제공되지 않으므로 생략 불가능합니다.
  • foo(a="Hello") - 저기여기에서 각각 "Hello"를 출력합니다.
  • foo(b="Hello") - (오류) a 인자는 let c = a;에서도 사용되므로 생략 불가능합니다.
  • foo(c="Hello") - (오류) a 인자는 let b = bar(a);에서도 사용되므로 생략 불가능합니다.
  • foo(d="Hello") - (오류) a 인자는 let c = a;에서도 사용되므로 생략 불가능합니다.
  • foo(a="Hello", b="World") - 여기에서 "Hello"를 출력합니다.
  • foo(a="Hello", c="World") - 저기에서 Hello를 출력하고 여기에서 "World"를 출력합니다.
  • foo(a="Hello", d="World") - 여기에서 "Hello"를 출력합니다. d를 주입하여 달리 참조되는 곳이 없는 b 선언이 제거되었습니다.
  • foo(b="Hello", c="World") - 여기에서 "World"를 출력합니다. a 인자가 생략됐지만 사용되는 곳이 없으므로 문제 없습니다.
  • foo(b="Hello", d="World") - (오류) bd에서만 사용되기 때문에 d 인자가 주어진다면 b를 주입할 필요가 없습니다.
  • foo(c="Hello", d="World") - 여기에서 "Hello"를 출력합니다.
  • baz(a="World") - (오류) a 변수는 함수 스코프가 아닌 곳에 선언돼있습니다.