Upgrade to Pro — share decks privately, control downloads, hide ads and more …

그럼에도 불구하고 Elixir

kakao
PRO
December 09, 2022

그럼에도 불구하고 Elixir

#함수형프로그래밍 #DeveloperExperience

이번 세션에는 카카오워크 서버 개발에 주로 사용하는 함수형 언어 엘릭서에 대해 소개해 드리고 여러 어려움에도 불구하고 저희가 엘릭서를 계속 사용하도록 이끌어준, 엘릭서의 실용주의적 특징들을 자세히 소개해드리도록 하겠습니다.

발표자 : walter.s
카카오엔터프라이즈에서 카카오워크 서버를 개발하는 walter 입니다. 실용주의를 추구하고 암벽을 오릅니다.

woogie.choi
카카오엔터프라이즈에서 카카오워크 서버를 개발하는 woogie 입니다. 재미있는 개발을 추구합니다.

kakao
PRO

December 09, 2022
Tweet

More Decks by kakao

Other Decks in Programming

Transcript

  1. 그럼에도 불구하고 Elixir
    서동현 walter.s 최병욱 woogie.choi


    카카오엔터프라이즈


    Copyright 2022. Kakao Corp. All rights reserved. Redistribution or public display is not permitted without written permission from Kakao.
    if(kakao)2022
    카카오워크 서버팀의 Elixir 실용주의 프로그래밍


    View Slide

  2. 1. 카카오워크와 Elixir 소개


    2. 실용주의 프로그래밍


    3. 실용주의 개발을 이끄는 Elixir의 특징들

    View Slide

  3. 카카오워크와 Elixir 소개

    View Slide

  4. 하이브리드 근무시대에 최적화된 종합 업무 플랫폼
    카카오워크 서비스 소개

    View Slide

  5. 함수형 언어 Elixir 소개
    - 카카오워크 서버의 주 언어 중 하나


    - 올해로 10주년을 맞은 신생 언어


    - 언어 설계부터 다양한 언어들의 장점을 토대로

    View Slide

  6. 함수형 언어 Elixir 소개
    - 신뢰성을 보장하는 Erlang 기반


    - 쉽고 직관적인 Ruby 문법과 유사


    - Clojure의 메타프로그래밍과 다형성1)

    View Slide

  7. 함수형 언어 Elixir 소개
    - 신생 언어이지만 높은 완성도와 안정성


    - 높은 버전 호환성


    - 앞으로도 언어의 큰 변경점 없을 것

    View Slide

  8. 카카오워크에서는 Elixir를
    - 서비스 초기부터 프로토타입을 릴리즈하면서 개발


    - 2년 넘게 엘릭서 서버들을 운영중


    - 엘릭서의 특성을 살릴 수 있는 곳에 활용하기 위해 노력


    - 메인 API 서버 등 여러 곳에 Elixir 사용 중

    View Slide

  9. 운영 레퍼런스 부족
    개발자 채용과 훈련
    Elixir를 사용하며 겪은 문제들

    View Slide

  10. - 다양한 언어와 기술스택 환경


    - 개발자가 언어를 넘나들며 개발


    - 객체지향에서 함수형으로 패러다임 전환의 어려움 2)
    개발자 채용과 훈련 문제

    View Slide

  11. - Erlang VM을 k8s 컨테이너 환경 위에서 운영


    - 분산 클러스터 환경에서 배포 운용, 최적화 노하우 부족


    - 최근에는 다양한 레퍼런스 참고 가능
    운영 노하우와 레퍼런스 부족

    View Slide

  12. - 선택의 순간들과 다양한 대안들


    - 비교적 레퍼런스가 풍부한 Java, Kotlin


    - 비슷하게 분산처리와 동시성에 강한 golang


    다른 길은 있었지만

    View Slide

  13. - 정기적인 Elixir 스터디


    - 한국 Elixir 밋업 참여


    - Elixir 오픈소스 라이브러리들에 컨트리뷰트


    - 더 잘 사용하기 위해 노력중
    그래도 선택은 Elixir

    View Slide

  14. 왜 그럼에도 불구하고 Elixir?

    View Slide

  15. Pragmatic Programming
    실용주의 프로그래밍과 개발자의 생산성

    View Slide

  16. 실용주의 프로그래머
    - 20년 넘게 개발자 스테디셀러


    - 실용주의적 가치관 강조


    - 책의 저자도 엘릭서 대표 에반젤리스트

    View Slide

  17. 적당히 괜찮으면서도, 변경이 쉬운 소프트웨어

    View Slide

  18. 코드 생산성
    개발자 생산성

    View Slide

  19. 코드 생산성
    개발자 생산성
    유지보수성 신뢰성

    View Slide

  20. 카카오워크가 경험하는 생산성
    - 최소 기능 제품(MVP) 까지 반년 이내


    - 지속적인 유지보수가 쉬운 코드 구조


    - 소프트웨어 결함 방어 및 배포된 코드 실시간 수정 가능

    View Slide

  21. 실용주의 개발을 이끄는 Elixir 특징들

    View Slide

  22. 1. 함수형 프로그래밍


    2. 메타프로그래밍과 개발자 경험


    3. Let it Crash!


    4. 개발 도구 생태계

    View Slide

  23. 1. 함수형 프로그래밍


    2. 메타프로그래밍과 개발자 경험


    3. Let it Crash!


    4. 개발 도구 생태계

    View Slide

  24. Elixir의 함수형 프로그래밍 특징
    - 변환 프로그래밍: 프로그램은 데이터를 변환하는 것이 중심4)


    - 데이터를 변환하는 것은 함수이며 함수들의 조합이 프로그램


    - 함수들을 작고 한가지 목적에만 집중하게 만들수록, 더 유연하고 테스트 쉬운 구조


    - 로직 설계의 변경은 함수의 재조합

    View Slide

  25. 파이프라인
    패턴 매칭
    Elixir의 변환 & 함수형 프로그래밍

    View Slide

  26. Pattern Matching
    - 패턴 매칭 = 구조 분해 할당(Destructuring) + 제어 흐름(Control
    fl
    ow)

    View Slide

  27. Pattern Matching
    - 함수형 언어에서의 선언적인 흐름제어


    - Elixir는 동적 타입의 패턴매칭
    def get_y(position) do


    case position do


    {x, y} -> {:ok, y}


    _other -> :error


    end


    end


    View Slide

  28. bot
    user
    send
    예제: 유저 or 봇의 메시지 전송
    send

    View Slide

  29. interface MessageSender {


    public void sendMessage(String message);


    }




    class User implements MessageSender {


    public void sendMessage(String message) {


    // ঐഐച ଼੐


    }


    }




    class Bot implements MessageSender {


    public void sendMessage(String message) {


    // ...


    }


    }

    View Slide

  30. defmodule MessageSender do


    # User


    def send_message(%{type: :user} = user, message) do


    # ঐഐച


    end


    # Bot


    def send_message(%{type: :bot} = bot, message) do


    # ...


    end


    end

    View Slide

  31. defmodule MessageSender do


    def send_message(%User{} = user, message) do


    # ঐഐച


    end


    def send_message(%Bot{} = bot, message) do


    # ...


    end


    end

    View Slide

  32. defmodule MessageSender do


    def send_message(%User{} = user, message) do


    # ঐഐച


    end


    def send_message(%Bot{} = bot, message) do


    # ...


    end


    def send_message(something_else, message) do


    # handle error?


    end


    end

    View Slide

  33. 파이프라인: Elixir의 파이프 연산자 |>
    - 유닉스의 파이프 명령어와 유사


    - 파이프 연산자로 연결된 구조: 파이프라인


    - 함수들의 조합
    "file.txt"


    |> File.read!()


    |> String.split("\n")


    |> Enum.sort()


    |> Enum.uniq()


    |> Enum.take(4)
    cat file.txt | sort | uniq | head -4
    Enum.take(Enum.uniq(Enum.sort(String.split(File.read!("file.txt"), "\n"))),4)

    View Slide

  34. 파이프라인 예제
    장기 미접속 유저들에게 리텐션 메일 보내기

    View Slide

  35. 파이프라인 예제: 장기 미접속 유저들에게 리텐션 메일 보내기
    1. 주어진 유저 목록


    2. 각 유저의 마지막 로그인 기록 조회


    3. 로그인 기록이 30일 이전인 유저들만 필터링


    4. 유저들의 이메일 주소와 이름만 추출


    5. 유저들에게 이메일 전송

    View Slide

  36. 로그인 기록 조회 이메일 전송
    파이프라인 예제: 장기 미접속 유저들에게 리텐션 메일 보내기
    유저 목록 필터링 추출

    View Slide

  37. 파이프라인 예제: 장기 미접속 유저들에게 리텐션 메일 보내기
    1. 주어진 유저 목록


    2. 각 유저의 마지막 로그인 기록 조회


    3. 로그인 기록이 30일 이전인 유저들만 필터링


    4. 유저들의 이메일 주소와 이름만 추출


    5. 유저들에게 이메일 전송
    def retention_users(user_list) do


    user_list # 1


    |> zip_with_login_records() # 2


    |> filter_login_before_days(30) # 3


    |> extract_email_and_names() # 4


    |> send_retention_emails() # 5


    end

    View Slide

  38. def retention_users(user_list) do


    user_list # 1


    |> zip_with_login_records() # 2


    |> filter_login_before_days(30) # 3


    |> extract_email_and_names() # 4


    |> send_retention_emails() # 5


    end
    def send_retention_emails([]), do: []


    def send_retention_emails({:error, reason} = error) do


    error


    end


    def send_retention_emails(user_list) when is_list(user_list) do


    # ...


    end

    View Slide

  39. SQL Query도 파이프라인
    # ౠ੿ orgী ࣘೠ ഝࢿ ਬ੷ ݾ۾ਸ ಕ੉૚ਵ۽ ઑഥೞח ௪ܻ


    def get_active_users_by_paging(org_id, cursor) do


    User


    |> active()


    |> by_org(org_id)


    |> after_cursor(cursor)


    # ...


    |> Repo.all()


    end


    defp active(query) do


    query |> where([u], u.status != :deleted)


    end


    defp by_org(query, org_id) do


    query |> where([u], u.org_id == ^org_id)


    end

    View Slide

  40. - Phoenix: 엘릭서의 웹 프레임워크


    - Phoenix의 모든 레이어는 아래와 같은 파이프라인으로 표현 가능 5)
    connection


    |> endpoint()


    |> router()


    |> pipelines()


    |> controller()
    프레임워크도 파이프라인이다

    View Slide

  41. - Phoenix의 모든 API는 커넥션을 첫번째 인자로 받으며 다른 함수를 호출


    - 파이프라인 구조로 프레임워크도 직관적 이해
    connection


    |> endpoint()


    |> router()


    |> pipelines()


    |> controller()
    프레임워크도 파이프라인이다

    View Slide

  42. - 함수형 프로그래밍: 변환 프로그래밍 관점


    - 패턴 매칭 + 파이프라인 : 직관적이고 유연한 스타일의 코드


    - 패턴 매칭: 데이터 형태에 집중하여 함수 설계


    - 파이프라인: 함수를 조합하는 방법이자 변환 프로그래밍의 핵심


    - 비즈니스 로직부터 프레임워크까지 파이프라인 구조
    함수형 프로그래밍 Recap

    View Slide

  43. - 개발자가 진짜 해결하고자 하는 문제에 집중하도록


    - 코드 구조를 단순화하고 변경이 쉽게
    함수형 프로그래밍과 실용주의

    View Slide

  44. 1. 함수형 프로그래밍


    2. 메타프로그래밍과 개발자 경험


    3. Let it Crash!


    4. 개발 도구 생태계

    View Slide

  45. 개발자 경험과 생산성
    - 개발자가 개발을 하며 겪는 경험의 만족도


    - 가능한 쉽고 직관적으로 이해하고 사용 가능해야 한다

    View Slide

  46. 개발자 경험과 생산성
    - UX와 차이: 개발자 숙련에 따라 더 다양한 선택지 제공


    - 좋은 개발자 경험은 개발 생산성을 높인다

    View Slide

  47. Stack Over
    fl
    ow 2022 Developer Survey
    Most Loved Languages6) Most Loved Frameworks7)

    View Slide

  48. Stack Over
    fl
    ow 2022 Developer Survey
    Most Loved Languages6) Most Loved Frameworks7)

    View Slide

  49. Phoenix의 도메인 특화 문법
    defmodule HelloWeb.Router do


    use Phoenix.Router


    pipeline :browser do


    plug :accepts, ["html"]


    plug :fetch_session


    plug :protect_from_forgery


    end


    scope "/", HelloWeb do


    pipe_through :browser


    get "/", PageController, :index


    resources "/users", UserController, only: [:index, :show, :new, :create]


    end


    end

    View Slide

  50. Elixir와 Erlang의 만족도 차이
    - Elixir는 Erlang과 많은 유사점

    View Slide

  51. Elixir와 Erlang의 만족도 차이
    Most Loved Languages
    - Elixir는 Erlang과 많은 유사점


    - 그러나 두 언어 사용자들의 큰 만족도 차이


    - 엘릭서의 편리하고 이해하기 쉬운 문법


    - 메타프로그래밍 덕분에 가능한 문법

    View Slide

  52. Elixir의 메타프로그래밍과 매크로
    - 메타프로그래밍: 코드로 코드를 작성하는 방법


    - 고수준의 우아한 문법(매크로)으로 저수준의 코드 조작


    - 매크로: 코드를 조작 가능한 함수


    - 언어의 코어 스펙이 작고 매크로를 활용해 스스로 확장되는 방식


    - 기본 문법들 대부분이 매크로

    View Slide

  53. def parse(data) do


    Logger.debug("Parse the data #{data}")


    # ...


    end
    매크로와 두마리 토끼: 성능과 우아함
    - 컴파일 타임에 코드 최적화: 성능 향상과 깔끔한 코드를 동시에


    - 엘릭서 표준 라이브러리의 Logger 모듈의 디버깅 함수

    View Slide

  54. 매크로와 두마리 토끼: 성능과 우아함
    - 엘릭서 1.12에서 추가된 tap, then 매크로
    "hello world"


    |> (fn x ->


    IO.puts(x)


    x


    end).()


    |> (&Regex.scan(~r/\w+/, &1)).()

    View Slide

  55. "hello world"


    |> tap(&IO.puts/1)


    |> then(&Regex.scan(~r/\w+/, &1))
    매크로와 두마리 토끼: 성능과 우아함
    - 엘릭서 1.12에서 추가된 tap, then 매크로

    View Slide

  56. 프레임워크의 DSL 문법
    defmodule MyApp.User do


    use Ecto.Schema


    schema "users" do


    field :name, :string


    field :nickname, :string


    field :avatar_url, :string


    end


    end
    defmodule MyAppWeb.Types.User do


    use Absinthe.Schema.Notation


    @desc "User Type"


    node object(:user) do


    field :name, non_null(:string)


    field :nickname, :string


    field :avatar_url, :string


    end


    end
    Ecto (Database Schema) Absinthe (GraphQL Schema)

    View Slide

  57. 메타프로그래밍: 직접 만드는 DSL
    defmodule MyWeb.Schemas.User do


    def schema do


    %Schema{


    title: "User",


    type: :object,


    required: [:id, :name, :status],


    properties: %{


    id: %Schema{type: :integer},


    name: %Schema{type: :string},


    status: %Schema{type: :string}


    }


    }


    end


    end
    defmodule MyWeb.Schemas.User do


    use OpenApiSpex.Schemax


    @required [:id, :name, :status]


    schema "User" do


    property :id, :integer


    property :name, :string


    property :status, :string


    end


    end
    라이브러리 기존 문법 직접 만든 DSL 문법

    View Slide

  58. - 강력하고 우아한 메타프로그래밍


    - 매크로를 통해 스스로 확장, 안정적이고 성숙된 문법들


    - 프레임워크들이 직관적인 선언형 문법 제공


    - 필요한 경우 적절히 사용하여 자유롭게 원하는 문법 창조


    - 단, 매크로는 양날의 검
    메타프로그래밍 Recap

    View Slide

  59. 메타프로그래밍
    함수형 프로그래밍

    View Slide

  60. 1. 함수형 프로그래밍


    2. 메타프로그래밍과 개발자 경험


    3. Let it Crash!


    4. 개발 도구 생태계

    View Slide

  61. 시스템 장애
    배포
    문제 발생
    야근

    View Slide

  62. 오류의 종류
    - NullPointException


    - IndexOutOfBoundsException


    - XXXException


    - 500 Internal server error


    - OutOfMemoryError


    - 개발자의 실수


    - ETC
    SOMETHING WENT WRONG!

    View Slide

  63. 방어적 프로그래밍

    View Slide

  64. 방어적 프로그래밍 예시
    def get_user(id) do


    Http.get(“https://api.account.com/users/#{id}")


    end


    View Slide

  65. def get_user(id) do


    if !is_integer(id) or id < 1 do


    raise IllegalArgumentException


    end


    try do


    user = Http.get(“https://api.account.com/users/#{id}")


    if is_nil(user) do


    raise NotFoundException


    end


    user


    catch


    error in HttpException -> raise CustomExternalException


    end


    end


    View Slide

  66. 방어적 프로그래밍의 단점
    - 복잡해진 코드로 인한 낮은 가독성


    - 코드 작성과 익셉션 정의 등으로 인한 많은 비용 발생


    - 그럼에도 예외가 없다고 확신 할 수 없음


    - 해결하려는 문제에 집중하지 못함
    SOMETHING WENT WRONG!
    def get_user(id) do


    if !is_integer(id) or id < 1 do


    raise IllegalArgumentException


    end


    try do


    user =


    Http.get(


    “https://api.account.com/users/#{id}”


    )


    if is_nil(user) do


    raise NotFoundException


    end


    user


    catch


    error in HttpException ->


    raise CustomExternalException


    end


    end


    View Slide

  67. 오류가 발생하면 크래시나도록 내버려 둔다
    Let it Crash!

    View Slide

  68. 액터 동시성 모델
    - 엘릭서에서 액터는 ‘프로세스’


    - 프로세스는 특정 작업을 수행


    - 서로 격리된 프로세스들은 메세지를 보내 소통


    - 프로세스는 ErlangVM위에서 돌아가며 자원이 매우 적게 듬

    View Slide

  69. 사용자 일괄 등록 예제
    1. 엑셀을 파싱하여 등록할 사용자 정보 목록 추출 (Parser)


    2. 사용자 등록 (Register)


    3. 등록 결과 메세지 전송 (Reporter)

    View Slide

  70. Parser Register
    사용자 일괄 등록 예제
    Reporter

    View Slide

  71. 사용자 일괄 등록 예제
    Parser Register

    View Slide

  72. 사용자 일괄 등록 예제
    Parser Register Reporter

    View Slide

  73. 사용자 일괄 등록 예제
    Manager
    Parser Register Reporter

    View Slide

  74. 사용자 일괄 등록 예제
    Manager
    Parser Register Reporter

    View Slide

  75. 사용자 일괄 등록 예제
    Manager
    Parser Register Reporter

    View Slide

  76. 사용자 일괄 등록 예제
    Manager
    Manager
    Parser Register Reporter

    View Slide

  77. 사용자 일괄 등록 예제
    Manager
    Register
    Queue

    View Slide

  78. 사용자 일괄 등록 예제
    Worker
    Manager
    Register
    Queue
    Reporter

    View Slide

  79. 사용자 일괄 등록 예제
    Worker
    Manager
    Register
    Queue
    Reporter

    View Slide

  80. 사용자 일괄 등록 예제
    Worker
    Manager
    Register
    Queue
    Reporter

    View Slide

  81. 사용자 일괄 등록 예제
    Worker
    Manager
    Register
    Queue
    Reporter

    View Slide

  82. 사용자 일괄 등록 예제
    Worker
    Manager
    Register
    Queue
    Reporter

    View Slide

  83. 사용자 일괄 등록 예제
    Worker
    Manager
    Register
    Queue
    Reporter

    View Slide

  84. 사용자 일괄 등록 예제
    Worker
    Manager
    Register
    Queue
    Reporter

    View Slide

  85. 사용자 일괄 등록 예제
    Reporter

    View Slide

  86. 사용자 일괄 등록 예제
    Manager
    Manager
    Parser Register Reporter
    Worker
    Queue

    View Slide

  87. Let it Crash! Recap
    - 오류를 잡기위한 방어적인 프로그래밍은 소모적


    - 그냥 크래시나게 두어라


    - 프로세스 관리 전략으로 스스로 복구하는 시스템 구성

    View Slide

  88. 적은 노력으로 스스로 복구하는 시스템을 구성합니다.
    Let it Crash! 실용주의

    View Slide

  89. 1. 함수형 프로그래밍


    2. 메타프로그래밍과 개발자 경험


    3. Let it Crash!


    4. 개발 도구 생태계

    View Slide

  90. 엘릭서는 좋은 개발자 경험에 진심이다.
    - 엘릭서 릴리즈에 개발자 경험을 강조8)


    - 표준 테스트 프레임워크


    - 대화형 쉘 IEx


    - 언제 어디서나 엘릭서를 실행 할 수 있는 Livebook

    View Slide

  91. 테스트 프레임워크
    - 엘릭서에 내장 된 표준 테스트 라이브러리 제공


    - 매크로 사용하여 간편한 문법
    test "elixir test example" do


    assert something_is_valid()


    assert expect_list() == actual_list()


    assert expect_error_log() =~ "error"


    end


    View Slide

  92. 테스트 프레임워크
    - 엘릭서에 내장 된 표준 테스트 라이브러리 제공


    - 매크로 사용하여 간편한 문법


    - document + test = doctest
    defmodule ExampleCode do


    @moduledoc """


    ৃܼࢲ ৘ઁ ௏٘ ݽٕੑפ׮.


    """


    @doc """


    ੋ੗ ف ѐܳ ੑ۱߉ই ف чਸ ؊ೞח ೣࣻ


    ## Examples


    iex> ExampleCode.sum(1, 2)


    3


    iex> ExampleCode.sum(1, -2)


    -1


    """


    def sum(a, b) do


    a + b


    end


    end


    View Slide

  93. 테스트 프레임워크
    - 엘릭서에 내장 된 표준 테스트 라이브러리 제공환


    - 매크로 사용하여 간편한 문법


    - document + test = doctest
    defmodule ExampleCode do


    @moduledoc """


    ৃܼࢲ ৘ઁ ௏٘ ݽٕੑפ׮.


    """


    @doc """


    ੋ੗ ف ѐܳ ੑ۱߉ই ف чਸ ؊ೞח ೣࣻ


    ## Examples


    iex> ExampleCode.sum(1, 2)


    3


    iex> ExampleCode.sum(1, -2)


    -1


    """


    def sum(a, b) do


    a + b


    end


    end


    View Slide

  94. IEx
    - 엘릭서 코드를 실행할 수 있는 대화형 쉘


    View Slide

  95. IEx
    - 엘릭서 코드를 실행할 수 있는 대화형 쉘


    - 코드 자동완성, 코드 문서 보기


    View Slide

  96. IEx
    - 엘릭서 코드를 실행할 수 있는 대화형 쉘


    - 코드 자동완성, 코드 문서 보기


    - 릴리즈된 어플리케이션에 접근 및 모듈 단위 코드 재컴파일


    View Slide

  97. Livebook
    - 웹브라우저에서 문서 작성 및 코드 실행


    - 마크다운 지원


    - 동시 작업


    View Slide

  98. 코드 작성과 실행을 같이

    View Slide

  99. 로그 출력 및 결과 확인

    View Slide

  100. 자동완성과 문서보기

    View Slide

  101. DB 연결, 쿼리 실행 및 결과 출력

    View Slide

  102. 다이어그램

    View Slide

  103. 공동 작업

    View Slide

  104. Livebook
    - 다양한 활용성 (임베디드, 머신러닝, 데이터 분석 등)


    - 엘릭서 설치가 필요 없는 데스크탑 앱 지원

    View Slide

  105. 개발 도구 생태계

    View Slide

  106. 함수형프로그래밍 메타프로그래밍 Let it Crash! 개발 도구 생태계

    View Slide

  107. 그래서 실용주의
    적당히 괜찮은 Elixir

    View Slide

  108. 그래서 Elixir
    실용주의 프로그래밍을 위하여

    View Slide

  109. 참고 문헌
    1) https:/
    /evrone.com/jose
    -
    valim
    -
    interview


    2) 데이비드 토마스, 『처음 배우는 엘릭서 프로그래밍』, 권두호 옮김, 한빛미디어, 2022년, p34


    3) 데이비드 토마스, 앤드류 헌트, 『실용주의 프로그래머(20주년 기념판)』, 정지용 옮김, 인사이트, 2022년, p.15


    4) 데이비드 토마스, 앤드류 헌트, 『실용주의 프로그래머(20주년 기념판)』, 정지용 옮김, 인사이트, 2022년, p.207


    5) Chris McCord, Bruce Tate, José valim, 『Programming Phoenix ≥ 1.4 』, The Pragmatic Programmers, p.18


    6) https:/
    /survey.stackover
    fl
    ow.co/2022/#section
    -
    most
    -
    loved
    -
    dreaded
    -
    and
    -
    wanted
    -
    programming
    -
    scripting
    -
    and
    -
    markup
    -
    languages


    7) https:/
    /survey.stackover
    fl
    ow.co/2022/#section
    -
    most
    -
    loved
    -
    dreaded
    -
    and
    -
    wanted
    -
    web
    -
    frameworks
    -
    and
    -
    technologies


    8) https:/
    /elixirforum.com/t/elixir
    -
    v1-14-0-released/49937

    View Slide