caffeine-library / Domain-Driven-Design

🌱 에릭 에반스의 '도메인 주도 설계'를 읽는 스터디
4 stars 0 forks source link

[discussion] 선언적 프로그래밍 #43

Closed wooyounggggg closed 1 year ago

wooyounggggg commented 1 year ago

토론 주제

선언적 프로그래밍

연관 이슈

27

토론 내용

연관 이슈 #27 의 마지막 부분에서 어떤 코드가 더 선언적으로 프로그램 된 것인지에 대해 잠깐 토론이 있었습니다. 이와 관련하여 궁금한 부분을 ChatGPT에 질문해 보았습니다. 아래의 대화록을 첨부합니다.


Q) Is OOP and declarative programming mutual exclusive? (OOP와 선언적 프로그래밍은 상호 배타적인가?)

A) No, Object-Oriented Programming (OOP) and Declarative Programming are not mutually exclusive. In fact, they are two different programming paradigms that can be used together in the same program to achieve different goals. OOP is a programming paradigm that is based on the concept of objects, which are instances of classes that encapsulate data and behavior. OOP focuses on defining classes, creating objects from those classes, and using those objects to interact with other objects in a program. Declarative programming, on the other hand, is a programming paradigm that focuses on describing what the program should do, rather than how to do it. In declarative programming, the programmer specifies the desired outcome of the program, and the program automatically determines how to achieve that outcome. Some programming languages, such as Java and C++, are primarily object-oriented languages, but they also support some declarative programming constructs, such as lambda expressions and functional interfaces. Similarly, some declarative programming languages, such as Prolog and SQL, also support object-oriented features. In conclusion, OOP and declarative programming are not mutually exclusive, and it is possible to use both paradigms together in the same program to achieve different goals.

A) 아니요, 객체 지향 프로그래밍(OOP)과 선언적 프로그래밍은 상호 배타적이지 않습니다. 실제로 이들은 서로 다른 목표를 달성하기 위해 동일한 프로그램에서 함께 사용할 수 있는 두 가지 프로그래밍 패러다임입니다. OOP는 데이터와 동작을 캡슐화하는 클래스의 인스턴스인 개체 개념을 기반으로 하는 프로그래밍 패러다임입니다. OOP는 클래스를 정의하고, 해당 클래스에서 개체를 만들고, 해당 개체를 사용하여 프로그램의 다른 개체와 상호 작용하는 데 중점을 둡니다. 반면에 선언적 프로그래밍은 프로그램이 수행 방법보다는 수행해야 하는 작업을 설명하는 데 중점을 둔 프로그래밍 패러다임입니다. 선언적 프로그래밍에서 프로그래머는 원하는 프로그램 결과를 지정하고 프로그램은 해당 결과를 달성하는 방법을 자동으로 결정합니다. Java 및 C++와 같은 일부 프로그래밍 언어는 주로 객체 지향 언어이지만 람다 식 및 기능적 인터페이스와 같은 일부 선언적 프로그래밍 구조도 지원합니다. 마찬가지로 Prolog 및 SQL과 같은 일부 선언적 프로그래밍 언어도 객체 지향 기능을 지원합니다. 결론적으로 OOP와 선언적 프로그래밍은 상호 배타적이지 않으며 동일한 프로그램에서 두 패러다임을 함께 사용하여 다른 목표를 달성할 수 있습니다.


Q) If so, is the act of encapsulating a function written in any imperative paradigm, encapsulating it into another object method, and calling it considered declarative? (그렇다면 임의의 명령형 패러다임으로 작성된 함수를 캡슐화하여 다른 객체 메서드에 캡슐화하고 호출하는 행위는 선언적이라고 보는가?)

A) No, encapsulating a function written in an imperative paradigm inside an object method and calling it is not considered declarative programming. Declarative programming is a programming paradigm that focuses on expressing what a program should accomplish, rather than how to accomplish it. Declarative programming often uses declarative constructs such as logical statements or equations to express the desired outcome. In declarative programming, the program describes a set of rules or constraints, and the system automatically derives the solution from those rules. On the other hand, encapsulating a function inside an object method and calling it is a feature of object-oriented programming (OOP). OOP is an imperative programming paradigm that focuses on defining classes and objects that encapsulate data and behavior. OOP typically involves defining methods that manipulate the data encapsulated within an object, and then invoking those methods on the object. Therefore, encapsulating a function within an object method and calling it is an imperative programming approach, and not a declarative one.

A) 아니요, 명령형 패러다임으로 작성된 함수를 개체 메서드 내부에 캡슐화하고 호출하는 것은 선언적 프로그래밍으로 간주되지 않습니다. 선언적 프로그래밍은 프로그램을 수행하는 방법보다는 프로그램이 수행해야 하는 것을 표현하는 데 중점을 둔 프로그래밍 패러다임입니다. 선언적 프로그래밍은 원하는 결과를 표현하기 위해 종종 논리적 명령문 또는 방정식과 같은 선언적 구조를 사용합니다. 선언적 프로그래밍에서 프로그램은 일련의 규칙 또는 제약 조건을 설명하고 시스템은 이러한 규칙에서 자동으로 솔루션을 도출합니다. 반면에 함수를 객체 메서드 안에 캡슐화하고 호출하는 것은 객체 지향 프로그래밍(OOP)의 기능입니다. OOP는 데이터와 동작을 캡슐화하는 클래스와 개체를 정의하는 데 중점을 둔 명령형 프로그래밍 패러다임입니다. OOP는 일반적으로 개체 내에 캡슐화된 데이터를 조작하는 메서드를 정의한 다음 개체에서 해당 메서드를 호출하는 작업을 포함합니다. 따라서 개체 메서드 내에서 함수를 캡슐화하고 호출하는 것은 선언적 프로그래밍 방식이 아니라 명령형 프로그래밍 방식입니다.


Q) I think the word 'declarative' is confusing. I know that the core of the declarative programming paradigm is to describe 'what' to do, not 'how'. If you 'declare' that you will use a specific method by encapsulating the method with an object, isn't it closer to declarative programming? ('선언적'이라는 단어가 헷갈리는 것 같다. 선언적 프로그래밍 패러다임의 핵심은 '어떻게'가 아니라 '무엇을' 해야 하는지를 기술하는 것으로 알고 있다. 메소드를 객체로 캡슐화하여 특정 메소드를 사용하겠다고 '선언'한다면 선언적 프로그래밍에 가깝지 않는가?)

A) You are correct that the word "declarative" can be confusing in this context, and that encapsulating a method inside an object could be seen as closer to declarative programming than typical imperative programming approaches. In object-oriented programming, encapsulating methods inside objects can help to promote a more declarative style of programming, where code is written in a way that is closer to describing what the program should do, rather than how to do it. By encapsulating related behavior and data within objects, OOP can provide a more modular and understandable structure to code. However, it is important to note that object-oriented programming is still considered an imperative programming paradigm, since it involves defining instructions and procedures for the computer to follow. In OOP, objects are manipulated through method calls that modify their state, which is an imperative approach. So while encapsulating methods inside objects can promote a more declarative style of programming, it does not necessarily make the program itself declarative. Ultimately, whether a program is considered declarative or not depends on whether its logic and structure are primarily focused on describing what should happen, rather than how it should happen.

A) 이 문맥에서 "선언적"이라는 단어가 혼동을 줄 수 있고 개체 내부의 메서드를 캡슐화하는 것이 일반적인 명령형 프로그래밍 접근 방식보다 선언적 프로그래밍에 더 가깝다고 볼 수 있다는 것이 맞습니다. 개체 지향 프로그래밍에서 개체 내부의 메서드를 캡슐화하면 보다 선언적인 프로그래밍 스타일을 촉진하는 데 도움이 될 수 있습니다. 여기서 코드는 프로그램이 수행하는 방법보다는 수행해야 하는 작업을 설명하는 방식에 더 가까운 방식으로 작성됩니다. 개체 내에서 관련 동작 및 데이터를 캡슐화함으로써 OOP는 코드에 보다 모듈화되고 이해하기 쉬운 구조를 제공할 수 있습니다. 그러나 개체 지향 프로그래밍은 컴퓨터가 따라야 할 명령과 절차를 정의하는 것과 관련되기 때문에 여전히 명령형 프로그래밍 패러다임으로 간주된다는 점에 유의해야 합니다. OOP에서 개체는 명령형 접근 방식인 상태를 수정하는 메서드 호출을 통해 조작됩니다. 따라서 객체 내부의 캡슐화 메서드는 보다 선언적인 프로그래밍 스타일을 촉진할 수 있지만 반드시 프로그램 자체를 선언적으로 만드는 것은 아닙니다. 궁극적으로 프로그램이 선언적이라고 간주되는지 여부는 프로그램의 논리와 구조가 어떻게 발생해야 하는가보다 발생해야 하는 것을 설명하는 데 주로 초점을 맞추는지 여부에 달려 있습니다.


Q) If so, can you give an example of code that is more declarative than using well-encapsulated objects?(그렇다면 잘 캡슐화된 객체를 사용하는 것보다 더 선언적인 코드의 예를 들어줘)

A) Sure! Here's an example of code written in a more declarative style using a functional programming paradigm:

const numbers = [1, 2, 3, 4, 5];
const result = numbers
  .filter(num => num % 2 === 0) // Select even numbers
  .map(num => num * 2) // Double each number
  .reduce((sum, num) => sum + num, 0); // Sum the numbers

console.log(result); // Output: 12

This code is written in a functional programming style, which is a declarative programming paradigm. In functional programming, functions are defined as expressions that take inputs and produce outputs, without changing any state outside of the function. In this code, we start with an array of numbers and use a chain of functional operations to transform the data into the desired output (the sum of the doubled even numbers). The filter() method selects only the even numbers, the map() method doubles each of those numbers, and the reduce() method adds them all up. Each of these operations is defined as a function that takes an input and produces an output, without modifying any state outside of the function. Compared to the example of encapsulating a method inside an object and calling it, this code is more declarative because it focuses on defining what operations should be performed on the data, rather than how those operations should be performed. The code does not need to worry about maintaining state or tracking object dependencies, making it easier to reason about and modify.

A) 이 코드는 선언적 프로그래밍 패러다임인 함수형 프로그래밍 스타일로 작성되었습니다. 함수형 프로그래밍에서 함수는 함수 외부의 상태를 변경하지 않고 입력을 받아 출력을 생성하는 표현식으로 정의됩니다. 이 코드에서는 숫자 배열로 시작하여 일련의 기능적 연산을 사용하여 데이터를 원하는 출력(두 배가 된 짝수의 합)으로 변환합니다. filter() 메서드는 짝수만 선택하고, map() 메서드는 각 숫자를 두 배로 늘리고, reduce() 메서드는 모두 더합니다. 이러한 각 작업은 함수 외부의 상태를 수정하지 않고 입력을 받아 출력을 생성하는 함수로 정의됩니다. 메서드를 객체 내부에 캡슐화하여 호출하는 예와 비교할 때 이 코드는 작업을 수행하는 방식보다 데이터에 대해 수행할 작업을 정의하는 데 중점을 두기 때문에 더 선언적입니다. 코드는 상태 유지 또는 개체 종속성 추적에 대해 걱정할 필요가 없으므로 더 쉽게 추론하고 수정할 수 있습니다.


Q) Then, the reason why you judged that the example you showed is 'a little more declarative' is because, from the developer's point of view, there is no need to check the inside of the calling function to see what it does?(그렇다면, 보여준 예시가 '조금 더 선언적'이라고 판단한 이유는 개발자 입장에서 호출하는 함수 내부를 확인하여 어떤 기능을 하는지 확인할 필요가 없기 때문인가?)

A) Yes, that's one way to think about it. In the example I showed, the code is more declarative because it clearly expresses what operations should be performed on the data, without needing to inspect the implementation details of any functions. In contrast, when encapsulating a method inside an object and calling it, the focus is on defining how to manipulate the data encapsulated within the object. This can make the code more difficult to understand and modify, because the logic is more tightly coupled to the object's state and the specific implementation details of the method. In a more declarative programming style, the focus is on defining what should happen to the data, rather than how it should happen. This can make the code easier to understand and modify, because the focus is on the desired outcome rather than the specific implementation details. However, it's important to note that there are trade-offs to both imperative and declarative programming styles, and the most appropriate approach will depend on the specific requirements and constraints of the project at hand.

A) 예, 그것은 그것에 대해 생각하는 한 가지 방법입니다. 내가 보여준 예에서 코드는 어떤 함수의 구현 세부 사항을 검사할 필요 없이 데이터에 대해 수행해야 하는 작업을 명확하게 표현하기 때문에 더 선언적입니다. 대조적으로, 객체 내부에 메소드를 캡슐화하고 호출할 때 초점은 객체 내에 캡슐화된 데이터를 조작하는 방법을 정의하는 데 있습니다. 이렇게 하면 논리가 개체의 상태 및 메서드의 특정 구현 세부 정보에 더 밀접하게 연결되기 때문에 코드를 이해하고 수정하기가 더 어려워질 수 있습니다. 보다 선언적인 프로그래밍 스타일에서는 데이터에 발생하는 방식보다는 데이터에 발생하는 방식을 정의하는 데 중점을 둡니다. 이렇게 하면 특정 구현 세부 정보가 아닌 원하는 결과에 초점을 맞추기 때문에 코드를 더 쉽게 이해하고 수정할 수 있습니다. 그러나 명령형 프로그래밍 스타일과 선언적 프로그래밍 스타일 모두에 장단점이 있으며 가장 적절한 접근 방식은 당면한 프로젝트의 특정 요구 사항과 제약 조건에 따라 달라집니다.


요약하면, 선언적 프로그래밍이란 개발자가 코드가 어떤 일이 일어날지를 생각하지 않아도 되기 때문에 위와 같은 stream api가 더 선언적이라고 판단하는 것 같습니다. 이에 대해 어떻게 생각하시는지 궁금합니다.

leejaeseung commented 1 year ago

저희 팀은 선언적 프로그래밍으로 코드를 짜는 노력을 많이 하고 있는데, 최근에 선언적이지 못한 코드와 선언적인 코드의 차이가 조금이나마 느껴졌던 경험이 있어 공유드립니다.ㅎㅎ

최근에 Stream에 궁금증이 생겨 관련 코드를 까보았는데 라이브러리나 프레임워크는 아무래도 성능이 우선이다보니 명령형으로 짜여져 있는 경우가 대부분이더라구요. 명령형 코드에 대해 느낀 점은 코드를 추적하기 힘들다(읽기 힘들다) 였습니다.

아래는 이번에 본 Stream 코드인데, while 문과 var 변수 사용으로 코드를 읽기 상당히 힘들었습니다.

final def unsafeCompletePollers(
      hub: internal.Hub[A],
      subscribers: Set[(internal.Hub.Subscription[A], MutableConcurrentQueue[Promise[Nothing, A]])],
      subscription: internal.Hub.Subscription[A],
      pollers: MutableConcurrentQueue[Promise[Nothing, A]]
    ): Unit = {
      var keepPolling = true
      val nullPoller  = null.asInstanceOf[Promise[Nothing, A]]
      val empty       = null.asInstanceOf[A]

      while (keepPolling && !subscription.isEmpty()) {  // 이 while 문이 언제 중지되는지 쉽게 파악할 수 있을까요..?
        val poller = pollers.poll(nullPoller)
        if (poller eq nullPoller) {
          subscribers.remove(subscription -> pollers)
          if (pollers.isEmpty()) keepPolling = false
          else subscribers.add(subscription -> pollers)
        } else {
          subscription.poll(empty) match {
            case null =>
              unsafeOfferAll(pollers, poller +: unsafePollAll(pollers))
            case a =>
              unsafeCompletePromise(poller, a)
              unsafeOnHubEmptySpace(hub, subscribers)
          }
        }
      }
    }

위 코드를 조금이나마 선언적으로 리팩토링해 보았는데(잘 한건진 모르겠지만..ㅎㅎ;;) 재귀 함수를 사용함으로써 while, var 가 사라졌고 각각의 행위에 함수명이 붙음으로써 좀 더 코드를 읽을 수 있게 된 것 같습니다.

final def unsafeCompletePollers(
      hub: internal.Hub[A],
      subscribers: Set[(internal.Hub.Subscription[A], MutableConcurrentQueue[Promise[Nothing, A]])],
      subscription: internal.Hub.Subscription[A],
      pollers: MutableConcurrentQueue[Promise[Nothing, A]]
    ): Unit = {
      val nullPoller  = null.asInstanceOf[Promise[Nothing, A]]
      val empty       = null.asInstanceOf[A]

      def pollSubscription() = subscription.poll(empty) match {
       case null => unsafeOfferAll(pollers, poller +: unsafePollAll(pollers))
       case a =>
          unsafeCompletePromise(poller, a)
          unsafeOnHubEmptySpace(hub, subscribers)
     }

     def recursivePoll(poller: Promise[Nothing, A]): Unit = {
       if(subscription.isEmpty()) return ()

       poller match {
        case nullPoller =>
            subscribers.remove(subscription -> pollers)
            if(pollers.isEmpty()) return () else recursivePoll(pollers.poll(nullPoller))
        case _ => 
            pollSubscription()
            recursivePoll(pollers.poll(nullPoller))
       }
     }

     recursivePoll(pollers.poll(nullPoller))
}
leejaeseung commented 1 year ago

선언적 프로그래밍에 대해 더 찾아보다가 좋은 글을 발견해 정리한 내용을 공유합니다! ㅎㅎ

명령적 vs 선언적 프로그래밍

현실 세계에서 명령형 vs 선언형 방식의 차이

명령형

Gone Fishin 간판 앞의 자리가 비어 있습니다. 나와 남편은 그 자리로 가서 앉겠습니다.

자리에 앉기까지의 과정. 즉, 어떻게 앉을 것인지를 서술 문제에 대해 어떻게 해결할지 해결 과정을 상대방(문제를 해결해주는 사람)에게 알려준다.

선언형

두 자리 주세요.

무엇을 원하는지 서술

문제에 대해 무엇을 원하는지 상대방(문제를 해결해주는 사람)에게 알려준다. → 상대방은 문제의 해결 방법을 알고 있다.

it’s important to realize that many declarative approaches have some sort of imperative abstraction layer.

  • 선언적 접근은 명령형 추상화 계층의 집합이다
  • 무엇을 해결해야 할 지만을 기술하고, 해결 방법은 추상화해 숨긴다.

코드 입장에서 상대방(문제를 해결해주는 사람)은 누굴까?

I draw the line between declarative and non-declarative at whether you can trace the code as it runs. Regex is 100% declarative, as it’s untraceable while the pattern is being executed.

  • 코드를 실행 중에 추적 가능한지 여부에 따라 선언적인지, 선언적이지 않은 지 나눠볼 수 있다
  • Regex 는 해당 패턴이 실행되면 추적할 수 없기 때문에 100% 선언적이다
  • Regex 는 그 자체로 선언 되었기 때문에 추적하지 않고 동작을 예측할 수 있다는 걸까?
  • 즉, Regex 의 동작이 예측 가능하게 추상화되었기 때문에 사용자는 내부 동작을 궁금해 할 필요 없이 결과를 예상할 수 있다.