본문 바로가기
학교/CPL

Lecture 12-13: Objects

by Hongwoo 2024. 4. 10.
반응형

목차

    Learning Objectives

    - Explain what an object is

    - Explain how objects are typed in Scala

    - Explain what the "self" is in object-oriented programming

    - Explain what delegation and late binding is in object-oriented programming

    - Desugar a language with simple objects into functions and mutation

    - Explain what the Expression Problem is, and how objected-oriented programming and functional programming solve different aspects of it

     

     

    Functional vs. Object-Oriented Programming

    Functional programming abstracts away irrelevant details of e.g. memory management

    This lets us program at a high level of abstraction and enables equational reasoning about programs

    But it also entails a performance overhead in many cases

    Object-oriented programming lets us define stateful abstractions that encapsulate state

    Object-oriented programming typically provides programmers with more control over run-time memory of programs than functional programming

     

     

    Example Object: Counter

     

     

    Definition of Object

    An object is meant to encapsulate in a coherent whole a piece of state together with some behavior that relies on that state. 

    The state is usually called fields, and the behavior is provided as a set of methods.

    Calling a method is often considered as message passing: we send a message to an object, and if it understands it, it executes the associated method.

     

     

     

    Objects, Messaging, and Encapsulation

     

     

     

    Object Typing and Sub-Typing

    Object Types in Scala

    Object types in Scala uniquely determine what fields and methods the object contains

    For example, the fields and methods accessible on instances of the following class is statically enforced by Scala's type system

     

    (new A).x

    → 1 returned

     

    (new A).y 

    → not a Runtime error, but error from type checking

    → This says variable y exists but you cannot access it

     

    (new A).z 

    → Not Found Error

     

    Subtyping in Java: inheritance (e.g. class C extends D)

     

     

    Structural Subtyping in Scala 3

     

    Method f takes input parameter called obj which has a structural type. Anything 

     

    f(new A)

    → returns val res1: Int = 1

    → As long as this scope contains method called x that returns Int, it works (as long as this has something named x that returns Int)

    만약에 x를 y로 바꾸면 No Such Method Error

    만약에 z면 Type Mismatch Error

     

     

     

    r.f()

    → 1 (r implements a structural type that has a field f. Anonymous class that has a single method that returns 1.

     

     

     

    s.g()

    → We cast to a supertype, the only thing that type checks it whether to check the object has a method f().

    → Not Found Error: value g is not a member of Object( f() : Int}

     

     

     

    Self

    Example that Returns Self

    Objects in Scala can reference themselves via the keyword this

    this: reference to the object we are calling the method on

    Whenever we are calling the method, we are implicitly initializing this keyword

    val res = Point(0, 0)

    val p2 = Point(1, 2)

    p2.compare(res)  → p2 returned  (더 큰 Point return)

     

     

    Delegation and Late Binding of Self

    Delegation

    Delegation: way of supporting code reuse in OOP languages

    In order to support reuse, objects can forward messages to delegatees

    We can think of inheritance as a kind of delegation

    Prototype delegation is also possible in e.g. JavaScript

     

     

    Inheritance in Scala

    Calling a method defined in a super-class is well-defined

     

    Example

     

    (new B).f

    → 42

     

     

    Late Binding in Scala

    Late binding of self provides better support for reuse

     

    Because I have defined the getPrice method such that it calls the overhead method of this, I can define subclasses/subobjects that override the overhead method but still allow me to call getPrice() to multiply the overhead of the subclass to the price of the superclass

     

    Late Binding of Self: I pass the subobject the method was originally called on. 

     

    (new ProdPro).getPrice()

    → 3 (this.overhead() is from the ProdPro  so this is ProdPro). 

    getPrice()에 this 키워드 없어도 똑같이 작동 (implicitly has this keyword)

    def 가 아니라 val 로 해도 똑같은 값 반환.

     

     

    Prototype Delegation in JavaScript

    The JavaScript language runtime allows the "delegate" of objects - i.e. the object to forward messages to - to be dynamically assigned during runtime

    Prototype based delegation: The objects are dynamically typed (no typechecker that checks the type of objects)

     

    obj.foo() → 1

    두번째에서는 obj.foo() → 2

     

     

    Late Binding and Prototype Inheritance in JavaScript

    Late binding of self provides better support for reuse.

     

    pp.getPrice()  → 3

    pp.__proto__ = { getPrice: () => 123 }
    pp.getPrice()  → 123

     

     

     

    What are Objects?

    The Essence of Objects

    Objects are structured collections of memory and functions that live on the heap

    Objects: co-data and co-recursive functions, with state

    → we can define objects by desugaring them into functions

     

     

    Javascript: count.js

    class Counter {
    
      #count = 0 // private fields are prefixed with #
      foo = 123
    
      inc() { // method
        this.#count = this.#count + 1
        return this.#count
      }
    
      dec() { // method
        this.#count = this.#count - 1
        return this.#count
      }
    
    }

     

    (new Counter()).#count
    // Uncaught Syntax Error: Private field '#count' must be declared
    
    (new Counter()).foo;
    // 123
    
    (new Counter()).inc();
    // 1
    
    (new Counter()).inc();
    // 1  - Creating a new object

     

     

    // Objects as closures
    // -------------------
    
    // OOPLAI:
    //
    // “An object is meant to encapsulate in a coherent whole a
    // piece of state (possibly, but not necessarily, mutable)
    // together with some behavior that relies on that state.”
    
    function CounterClo() {
      var v = 1
      return [ () => { v = v + 1; return v }
             , () => { v = v - 1; return v } ]
    }
    
    // try:
    // CounterClo()[0]()  --> 2
    // var p = clo()
    // p[0]()  --> 2
    // p[0]()  --> 3
    // p[1]()  --> 2
    // p.v  --> 2

     

     

    // Methods as messages
    // -------------------
    
    // Methods are invoked by sending messages to the closure
    
    function CounterObj() {
      var v = 1
      return (msg, args) => {
        if (msg == "inc") {
          v = v + 1
          return v
        } else if (msg == "dec") {
          v = v - 1
          return v
        } else if (msg == "incBy") {
          v = v + args[0]
          return v
        } else {
          throw new Error("The object only understands the following messages: inc/0, dec/0, incBy/1")
        }
      }
    }
    
    // q = CounterObj()
    // q("inc", []) --> 2
    // q("inc", []) --> 3
    // q("dec", []) --> 2
    // q("incBy", []) --> NaN
    // q("incBy", [20]) --> 22

     

     

    // Methods as messages, with closures
    // ----------------------------------
    
    // Methods are returned when sending a message to the closure. What you need to do in the lab
    
    function CounterObjClo() {
      var v = 1
      return (msg) => {
        if (msg == "inc") { return () => {
          v = v + 1
          return v
        } } else if (msg == "dec") { return () => {
          v = v - 1
          return v
        } } else if (msg == "incBy") { return (n) => {
          v = v + n
          return v
        } } else {
          throw new Error("The object only understands the following messages: inc/0, dec/0, incBy/1")
        }
      }
    }
    
    // if msg == "inc", it returns a closure that does not take any input.
    
    // r = CounterObjClo()
    // r("inc")()  --> 2
    // foo = r("inc")
    // foo() --> 3

     

    To deal with delegation, forward messages to delegatees:

    // Methods as messages, with closures
    // ----------------------------------
    
    // Objects the message "foo"
    function FooObj() {
      return (msg) => {
        if (msg == "foo") { return () => return "foo" }
        else {
          throw new Error("Dont know")
        }
      }
    }
    
    function CounterObjCloSup(superobj) {
      var v = 1
      return (msg) => {
        if (msg == "inc") { return () => {
          v = v + 1
          return v
        } } else if (msg == "dec") { return () => {
          v = v - 1
          return v
        } } else if (msg == "incBy") { return (n) => {
          v = v + n
          return v
        } } else {
          return superobj(msg)
        }
      }
    }
    
    
    s = CounterObjCloSup(FooObj())
    s("inc")() --> 2
    s("dec")() --> 1
    s("foo")() --> "foo"

     

     

     

    Javascript: proto.js

    Function: lambda, closure: run-time representation of a lambda, method:

     

    // Late binding of `this` affords reuse of functionality
    // with self-awareness
    // -----------------------------------------------------
    
    
    class Prod {
    
      #price = 2
    
      overhead() { return 1.0 }
      getPrice() { return this.overhead()*this.#price } // sends a message to "self"
    
    }
    
    class ProdPro extends Prod {
    
      overhead() { return 1.5 }
    
    }
    
    
    // try:
    
    // var p = new Prod
    // p.getPrice()  --> 2
    
    // var q = new ProdPro
    // q.getPrice()  --> 3

     

    Even though I'm calling getPrice() on the superclass, the notion of self which is "this" is the instance in the subclass.

     

     

    // Objects as closures with late binding of self
    // ---------------------------------------------
    
    // First take: try using same encoding as previously.
    
    function ProdObjClo0() {
      var price = 2
      var self
      self = (msg) => {
        if (msg == "overhead") { return () => 1.0 }
        else if (msg == "getPrice") {
          return () => self("overhead")()*price }
        else {
          throw new Error("The object only understands the following messages: overhead/0, getPrice/0")
        }
      }
      return self
    }
    
    
    
    function ProdProObjClo0() {
      var delegate = ProdObjClo0()
      var self
      self = (msg) => {
        if (msg == "overhead") { return () => 1.5 }
        else { return delegate(msg) }
      }
      return self
    }
      
    // try:
    
    // var p = ProdObjClo0()
    // p("getPrice")()  --> 2
    
    // var q = ProdProObjClo0()
    // q("overhead")() --> 1.5 
    // q("getPrice")()  --> 2

     

     

    // Second take: pass self as explicit parameter when
    // invoking methods
    // -------------------------------------------------
    
    function ProdObjClo() {
      var price = 2
      return (self) => (msg) => {
        if (msg == "overhead") { return () => 1.0 }
        else if (msg == "getPrice") { return () => self(self)("overhead")()*price }
        else {
          throw new Error("The object only understands the following messages: overhead/0, getPrice/0")
        }
      }
    }
    
    function ProdProObjClo() {
      var delegate = ProdObjClo()
      return (self) => (msg) => {
        if (msg == "overhead") { return () => 1.5 }
        else { return delegate(self)(msg) }
      }
    }
    
    // try:
    
    // var p = ProdObjClo()
    // p(p)("getPrice")()  --> 2
    
    // var q = ProdProObjClo()
    // q(q)("getPrice")()  --> 3
    
    // sugar for invoking a message, passing self:
    
    function invoke(receiver, msg) {
      return receiver(receiver)(msg)
    }

     

     

     

    The Expression Problem

    The goal is to define a datatype by cases, where one can add new cases to the datatype and new functions over the datatype, without recompiling existing code, and while retaining static type safety (e.g. no casts)

     

     

    The Expression Problem - Functions + Algebraic Data Types

    ADTs

     

     

    Extending with a New Function (typeOf)

     

     

     

    Extending with a New Case

     

     

     

    The Expression Problem - Object Oriented Programming

     

     

    Extending with a New Case (If)

     

     

     

    Extending with a New Function (typeOf)

     

     

     

    Summary

     

     

     

    반응형

    '학교 > CPL' 카테고리의 다른 글

    CPL 5: Parallel and Concurrent  (0) 2024.04.09
    Lecture 11: Type Checking  (1) 2024.04.06
    Lecture 9-10: Parallel and Concurrent  (0) 2024.04.06
    CPL 4: Mutation and State  (0) 2024.04.05
    Lecture 7-8: Mutation and State  (0) 2024.04.01

    댓글