익숙한 편견은 당신을 뒤로하고 있습니다 : 지금은 화살표 기능을 받아 들일 시간입니다

“앵커”— 액터 212 — (CC BY-NC-ND 2.0)

나는 생계를 위해 JavaScript를 가르칩니다. 최근에 저는 커리큘럼 주위를 뒤섞어 커리 화살표 기능을 처음 몇 수업 내에서 더 빨리 가르쳤습니다. 나는 매우 귀중한 기술이기 때문에 커리큘럼에서 더 일찍 옮겼으며, 학생들은 내가 생각했던 것보다 훨씬 빨리 화살표로 카레를 집어 들었습니다.

그들이 그것을 이해하고 더 일찍 이용할 수 있다면, 왜 일찍 가르치지 않겠습니까?

참고 : 내 강좌는 코드를 한 번도 접한 적이없는 사람들을위한 것이 아닙니다. 대부분의 학생들은 최소한 몇 개월 동안 코딩을 (자체적으로, 부트 캠프에서 또는 전문적으로) 보낸 후 참여합니다. 그러나 경험이 없거나 거의없는 많은 주니어 개발자들이 이러한 주제를 빠르게 선택하는 것을 보았습니다.

많은 학생들이 1 시간의 수업으로 커리 화살표 기능에 익숙해 지는 것을 보았습니다. "Eric Elliott로 JavaScript 배우기"회원 인 경우 지금 55 분 ES6 Curry & Composition 강의를 볼 수 있습니다.

학생들이 얼마나 빨리 그것을 가져 와서 새로운 카레 파워를 휘두르기 시작했는지를 보면서, 나는 트위터에 커리 화살표 기능을 게시 할 때 약간 놀랐습니다. Twitterverse는 "읽을 수없는"코드를 사용한다는 생각에 분노로 반응합니다. 그것을 유지해야 할 사람들.

먼저, 우리가 말하는 것에 대한 예를 들어 보겠습니다. 내가 백래시를 처음 발견 한 것은이 함수에 대한 트위터의 응답이었습니다.

const secret = msg => () => msg;

트위터의 사람들이 사람들을 혼란스럽게한다고 비난하면서 충격을 받았습니다. ES6에서 카레 기능을 표현하는 것이 얼마나 쉬운지를 보여주기 위해 해당 기능을 작성했습니다. JavaScript에서 생각할 수있는 가장 간단한 실용적인 응용 프로그램 및 클로저 표현입니다. (관련 :“폐쇄 란 무엇인가?”).

다음 함수 표현식과 같습니다.

const 비밀 = 함수 (msg) {
  리턴 함수 () {
    msg를 반환;
  };
};

secret ()은 msg를 가져오고 msg를 리턴하는 새 함수를 리턴하는 함수입니다. msg의 값을 secret ()에 전달하는 값으로 고정하기 위해 클로저를 활용합니다.

사용 방법은 다음과 같습니다.

const mySecret = 비밀 ( 'hi');
나의 비밀(); // '안녕'

“이중 화살표”는 사람들을 혼란스럽게합니다. 이것이 사실임을 확신합니다.

익숙한 인라인 화살표 함수는 카레 함수를 JavaScript로 표현하는 가장 읽기 쉬운 방법입니다.

많은 사람들이 긴 형식이 짧은 형식보다 읽기 쉽다고 나에게 주장했습니다. 그들은 부분적으로는 맞지만 대부분은 틀 렸습니다. 화살표 기능에 익숙하지 않은 사람에게는 더 간결하고 더 명확하지만 읽기 쉽지 않습니다.

트위터에서 본 이의 제기는 학생들이 즐기는 부드러운 학습 경험으로 인해 혼란스럽지 않았습니다. 내 경험상 학생들은 물고기가 물에 가져가는 것과 같이 카레 화살표 기능을 수행합니다. 그것들을 배우고 며칠 안에 그들은 화살표가있는 것입니다. 그들은 모든 종류의 코딩 문제를 해결하기 위해 손쉽게 슬링합니다.

화살표 기능이 몇 시간의 수업과 학습 세션을 통해 학습에 대한 초기 투자를 한 후에 배우고, 읽고, 이해하기에 "열심"하다는 표시는 보이지 않습니다.

그들은 전에 본 적이없는 카레 화살표 기능을 쉽게 읽고 무슨 일이 일어나고 있는지 설명 해줍니다. 내가 그들에게 도전을 제시 할 때 그들은 자연스럽게 자신의 글을 씁니다.

즉, 커리 화살표 기능을 보는 데 익숙해 지더라도 아무런 문제가 없습니다. 이 문장을 읽는 것처럼 쉽게 읽을 수 있으며 이해는 버그가 적은 훨씬 간단한 코드에 반영됩니다.

일부 사람들이 레거시 함수 표현식을 "쉽게"읽는다고 생각하는 이유

친숙성 편향은 측정 가능한 인간인지 편향으로, 더 나은 옵션을 알고 있음에도 불구하고 자기 파괴적인 결정을 내릴 수 있습니다. 우리는 편안함과 습관에서 더 나은 패턴을 알고 있음에도 불구하고 동일한 오래된 패턴을 계속 사용합니다.

훌륭한 책인“실행 취소 프로젝트 : 우리의 마음을 바꾼 우정”에서 친숙성 편견 (그리고 우리가 스스로를 속이는 다른 많은 방법들)에 대해 더 많이 배울 수 있습니다. 이 책은 모든 소프트웨어 개발자에게 읽어야합니다. 다양한인지 적 함정에 빠지지 않도록보다 비판적으로 사고하고 가정을 테스트해야합니다. 이러한인지 적 함정이 어떻게 발견되었는지에 대한 이야기도 정말 좋습니다. .

레거시 함수 표현식으로 인해 코드에서 버그가 발생할 수 있음

오늘 저는 사람들이 번역하지 않고도 오래된 브라우저에서 사용할 수있는 오픈 소스 모듈로 게시 할 수 있도록 ES6에서 ES5로 커리 화살표 기능을 다시 작성했습니다. ES5 버전이 충격을 받았습니다.

ES6 버전은 간단하고 짧으며 우아했습니다. 단 4 줄입니다.

필자는 이것이 화살표 기능이 우수하고 사람들이 자신의 나쁜 습관처럼 레거시 기능을 포기해야한다는 것을 트위터에 증명할 수있는 기능이라고 생각했습니다.

그래서 트윗했다 :

이미지가 작동하지 않을 경우를위한 기능의 텍스트는 다음과 같습니다.

// 화살표로 카레
const composeMixins = (... mixins) => (
  인스턴스 = {},
  믹스 = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x)
) => 믹스 (... mixins) (인스턴스);
// ES5 스타일과 비교
var composeMixins = 함수 () {
  var mixins = [] .slice.call (인수);
  리턴 기능 (인스턴스, 믹스) {
    if (! instance) instance = {};
    if (! mix) {
      믹스 = 함수 () {
        var fns = [] .slice.call (인수);
        리턴 함수 (x) {
          리턴 fns.reduce (함수 (acc, fn) {
            리턴 fn (acc);
          }, x);
        };
      };
    }
    return mix.apply (null, mixins) (인스턴스);
  };
};

문제의 함수는 일반적으로 함수를 구성하는 데 사용되는 표준 함수 프로그래밍 유틸리티 인 pipe () 주위의 간단한 래퍼입니다. pipe () 함수는 lodash / flow로 lodash에, R.pipe ()로 Ramda에 존재하며, 여러 기능 프로그래밍 언어로 된 자체 연산자도 있습니다.

함수형 프로그래밍에 익숙한 모든 사람에게 친숙해야합니다. 기본 종속성과 마찬가지로 Reduce.

이 경우 기능적인 믹스 인을 작성하는 데 사용되지만 관련이없는 세부 사항 (및 다른 전체 블로그 게시물)입니다. 중요한 세부 사항은 다음과 같습니다.

이 함수는 여러 기능 믹스 인을 가져 와서 조립 라인처럼 파이프 라인에서 하나씩 적용하는 함수를 반환합니다. 각 기능 믹스 인은 인스턴스를 입력으로 가져와 파이프 라인의 다음 함수로 전달하기 전에 인스턴스를 가져옵니다.

인스턴스를 생략하면 새 객체가 생성됩니다.

때로는 믹스 인을 다르게 구성하고 싶을 수도 있습니다. 예를 들어 pipe () 대신 compose ()를 전달하여 우선 순위를 반대로 할 수 있습니다.

비헤이비어를 커스터마이즈 할 필요가 없다면 기본값을 그대로두고 표준 pipe () 비헤이비어를 가져 오면됩니다.

그냥 사실

가독성에 대한 의견은 다음 예와 관련된 객관적인 사실입니다.

  • ES5 및 ES6 함수 표현식, 화살표 또는 기타 모두에서 다년간의 경험이 있습니다. 친근감 편견은이 데이터에서 변수가 아닙니다.
  • 몇 초 안에 ES6 버전을 썼습니다. 여기에는 제로 버그가 포함되어 있습니다 (내가 아는 것 – 모든 단위 테스트를 통과 함).
  • ES5 버전을 작성하는 데 몇 분이 걸렸습니다. 최소한 시간이 더 걸립니다. 분 대 초. 함수 들여 쓰기에서 두 번 자리를 잃었습니다. 나는 3 개의 버그를 썼다. 모두 디버깅하고 수정해야했다. 그중 두 가지는 console.log ()에 의존하여 무슨 일이 일어나고 있는지 파악해야했습니다.
  • ES6 버전은 4 줄의 코드입니다.
  • ES5 버전은 21 줄입니다 (17 개는 실제로 코드를 포함합니다).
  • 지루한 자세에도 불구하고 ES5 버전은 실제로 ES6 버전에서 사용 가능한 정보 충실도를 잃어 버립니다. 더 길지만 의사 소통이 적습니다. 자세한 내용은 계속 읽으십시오.
  • ES6 버전에는 함수 매개 변수에 대한 2 개의 스프레드가 있습니다. ES5 버전은 스프레드를 생략하고 대신 암시 적 인수 객체를 사용하여 함수 서명의 가독성을 떨어 뜨립니다 (충실도 다운 그레이드 1).
  • ES6 버전은 함수 시그니처에서 믹스의 기본값을 정의하므로 파라미터의 값임을 분명히 알 수 있습니다. ES5 버전은 그 세부 사항을 숨기고 대신 기능 본문 안쪽에 숨 깁니다. (충실도 다운 그레이드 2).
  • ES6 버전에는 들여 쓰기 수준이 2 단계 밖에 없으므로 읽을 수있는 구조를 명확하게 설명 할 수 있습니다. ES5 버전에는 6이 있으며 함수 구조의 가독성을 높이기보다는 중첩 수준이 불분명합니다 (충실도 다운 그레이드 3).

ES5 버전에서 pipe ()는 함수 본문의 대부분을 차지하므로 인라인으로 정의하기에는 약간 미쳤습니다. ES5 버전을 읽을 수있게하려면 별도의 기능으로 분리해야합니다.

var pipe = 함수 () {
  var fns = [] .slice.call (인수);
  리턴 함수 (x) {
    리턴 fns.reduce (함수 (acc, fn) {
      리턴 fn (acc);
    }, x);
  };
};
var composeMixins = 함수 () {
  var mixins = [] .slice.call (인수);
  리턴 기능 (인스턴스, 믹스) {
    if (! instance) instance = {};
    if (! mix) mix = 파이프;
    return mix.apply (null, mixins) (인스턴스);
  };
};

이것은 분명히 더 읽기 쉽고 이해하기 쉽습니다.

ES6 버전에 동일한 가독성 "최적화"를 적용하면 어떻게되는지 봅시다 :

const 파이프 = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x);
const composeMixins = (... mixins) => (
  인스턴스 = {},
  믹스 = 파이프
) => 믹스 (... mixins) (인스턴스);

ES5 최적화와 마찬가지로이 버전은 더 장황합니다 (이전에 없었던 새로운 변수를 추가합니다). ES5 버전과 달리 파이프 정의를 추상화 한 후에는이 버전을 쉽게 읽을 수 없습니다. 결국, 함수 서명에서 변수 이름이 명확하게 할당되었습니다 : mix.

믹스 정의는 이미 자체 라인에 포함되어 있으므로 독자가 종료 위치와 나머지 기능이 계속되는 위치에 대해 혼동하지 않을 수 있습니다.

이제 우리는 1 대신에 같은 것을 나타내는 2 개의 변수를 가지고 있습니다. 분명하지 않습니다.

그렇다면 동일한 기능을 추상화하여 ES5 버전이 더 나은 이유는 무엇입니까?

ES5 버전은 분명히 더 복잡하기 때문입니다. 그 복잡성의 근원은이 문제의 핵심입니다. 나는 복잡성의 원인이 구문 잡음으로 귀결되고 구문 잡음이 도움이 아니라 함수의 의미를 모호하게한다고 주장한다.

기어를 변속하고 더 많은 변수를 제거합시다. 두 예제 모두에 ES6을 사용하고 화살표 함수와 레거시 함수 표현식 만 비교해 보겠습니다.

var composeMixins = 함수 (... mixins) {
  리턴 기능 (
    인스턴스 = {},
    믹스 = 함수 (... fns) {
      리턴 함수 (x) {
        리턴 fns.reduce (함수 (acc, fn) {
          리턴 fn (acc);
        }, x);
      };
    }
  ) {
    믹스 반환 (... mixins) (인스턴스);
  };
};

이것은 훨씬 더 읽기 쉽습니다. 변경된 것은 휴식과 기본 매개 변수 구문을 활용한다는 것입니다. 물론,이 버전을보다 읽기 쉽게하려면 휴식과 기본 구문에 익숙해야하지만, 그렇지 않은 경우에도이 버전이 여전히 덜 혼란 스럽습니다.

그것은 많은 도움이되었지만,이 버전은 여전히 ​​자신의 함수로 pipe ()를 추상화하는 것이 분명히 도움이 될 정도로 충분히 어수선하다는 것이 분명합니다.

const 파이프 = 함수 (... fns) {
  리턴 함수 (x) {
    리턴 fns.reduce (함수 (acc, fn) {
      리턴 fn (acc);
    }, x);
  };
};
// 레거시 함수 표현식
const composeMixins = 함수 (... mixins) {
  리턴 기능 (
    인스턴스 = {},
    믹스 = 파이프
  ) {
    믹스 반환 (... mixins) (인스턴스);
  };
};

그게 더 낫지? 이제 믹스 할당은 한 줄만 차지하므로 함수의 구조가 훨씬 더 명확 해졌지만 여전히 내 취향에 맞는 구문 노이즈가 너무 많습니다. composeMixins ()에서는 한 함수가 끝나고 다른 함수가 시작되는 곳을 한눈에 알 수 없습니다.

함수 본문을 호출하는 대신 해당 함수 키워드가 시각적으로 주위의 식별자와 혼합되어있는 것 같습니다. 내 기능에 숨어있는 기능이 있습니다! 파라미터 서명은 어디에서 끝나고 함수 본문은 시작됩니까? 면밀히 살펴보면 알아낼 수 있지만 시각적으로는 명확하지 않습니다.

함수 키워드를 제거하고 주변 식별자와 혼합되는 반환 키워드를 작성하는 대신 큰 팻 화살표 =>로 시각적으로 가리켜 서 반환 값을 호출 할 수 있다면 어떨까요?

우리가 할 수 있고, 다음과 같이 보입니다.

const composeMixins = (... mixins) => (
  인스턴스 = {},
  믹스 = 파이프
) => 믹스 (... mixins) (인스턴스);

이제 무슨 일이 일어나고 있는지 분명해야합니다. composeMixins ()는 임의의 수의 믹스 인을 가져오고 두 개의 선택적 매개 변수 인 인스턴스와 믹스를 취하는 함수를 리턴하는 함수입니다. 구성된 믹스 인을 통해 파이핑 인스턴스의 결과를 반환합니다.

한가지 더… 파이프 ()에 동일한 최적화를 적용하면 마술처럼 하나의 라이너로 변환됩니다.

const 파이프 = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x);

한 줄로 그 정의를 사용하면 자체 기능으로 추상화하는 이점이 명확하지 않습니다. 이 함수는 Lodash, Ramda 및 기타 여러 라이브러리에 유틸리티로 존재하지만 다른 라이브러리를 가져 오는 오버 헤드의 가치가 있습니까?

자체 라인으로 가져갈 가치가 있습니까? 아마. 그것들은 실제로 두 가지 다른 기능이며, 분리하면 더 명확 해집니다.

반면, 인라인으로 설정하면 매개 변수 시그니처를 볼 때 유형 및 사용 예상이 명확 해집니다. 인라인으로 만들면 어떻게됩니까?

const composeMixins = (... mixins) => (
  인스턴스 = {},
  믹스 = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x)
) => 믹스 (... mixins) (인스턴스);

이제 원래 기능으로 돌아갑니다. 그 과정에서 우리는 아무런 의미도 버리지 않았습니다. 실제로 매개 변수와 기본값을 인라인으로 선언하여 함수 사용 방법 및 매개 변수 값에 대한 정보를 추가했습니다.

ES5 버전의 추가 코드는 모두 노이즈였습니다. 구문 노이즈. 카레 화살표 기능에 익숙하지 않은 사람들을 적응시키는 것 외에는 유용한 목적을 달성하지 못했습니다.

커리 화살표 기능에 익숙해지면 잃어 버릴 구문이 적기 때문에 원본 버전을 더 잘 읽을 수 있어야합니다.

버그를 숨길 수있는 표면적이 훨씬 적기 때문에 오류가 발생하기 쉽습니다.

화살표 기능으로 업그레이드하면 발견되고 제거 될 레거시 기능에 숨어있는 많은 버그가 있다고 생각합니다.

또한 ES6에서 사용 가능한 간결한 구문을 더 많이 수용하고 선호하는 법을 배운다면 팀의 생산성이 크게 향상 될 것으로 생각합니다.

때로는 명시 적으로 표현하면 이해하기 쉬운 것이 사실이지만, 일반적으로 코드가 적을수록 좋습니다.

더 적은 코드로 같은 것을 달성하고 의미를 희생하지 않고 더 많은 의사 소통을 할 수 있다면 객관적으로 더 좋습니다.

차이점을 아는 열쇠는 의미입니다. 더 많은 코드가 더 많은 의미를 부여하지 못하면 해당 코드가 존재하지 않아야합니다. 이 개념은 매우 기본적이며 자연어에 대한 잘 알려진 스타일 지침입니다.

동일한 스타일 지침이 소스 코드에 적용됩니다. 그것을 받아들이면 코드가 더 좋아질 것입니다.

하루가 끝나면 어둠 속의 빛. ES6 버전을 읽을 수 없다는 또 다른 트윗에 대한 응답으로 :

ES6, 커리 및 기능 구성에 익숙해 질 시간입니다.

다음 단계

“Eric Elliott로 JavaScript 배우기”회원은 지금 55 분 ES6 Curry & Composition 강의를 볼 수 있습니다.

회원이 아닌 경우 누락 된 것입니다.

Eric Elliott는“JavaScript 응용 프로그램 프로그래밍”(O'Reilly) 및“Eric Elliott로 JavaScript 배우기”의 저자입니다. 그는 Adobe Systems, Zumba Fitness, Wall Street Journal, ESPN, BBC 및 Usher, Frank Ocean, Metallica 등을 포함한 최고의 레코딩 아티스트를위한 소프트웨어 경험에 기여했습니다.

그는 세계에서 가장 아름다운 여성과 함께 샌프란시스코 베이 지역에서 대부분의 시간을 보냅니다.