ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Programming Languages PartC Week2学习笔记——OOP(面向对象) vs FD(函数式)

2022-09-14 21:01:11  阅读:213  来源: 互联网

标签:PartC end Programming return Value Languages other new def


@

目录

OOP Versus Functional Decomposition

面向对象与函数式(或过程式)编程的分解对比

image-20220627160043852

例子:不同的变量和操作构成了二维矩阵。

面向对象和函数式的思维方式不同。

image-20220627160316578

函数式编程的做法:通过函数填充每一列

image-20220627163410687

image-20220627163606273

image-20220627163657011

面向对象编程:通过类填充每一行

image-20220627163744848

对Ruby而言:

image-20220627163822498

image-20220627163842386

image-20220627163853899

image-20220627163903848

对Java而言:

image-20220627164220840

image-20220627164231036

image-20220627164304466

image-20220627164313562

所以,事实上函数式编程FP和面向对象编程OOP都能做到同样的事情,但他们用了两种几乎相反的方式。具体使用什么方式,取决于做的事情和个人喜好。

image-20220627164416300

Adding Operations or Variants

这一节涉及到对已有程序的维护迭代。例如分别以FP和OOP的视角来看增加操作或变量。

对于FP:

image-20220629140153744

代码中增加方法非常容易,只需要增加一个函数即可,但增加变量对象就相对麻烦一些,不仅需要在原有定义的结构中添加新的变量,而且需要修改前面定义的所有方法,让其适应新的变量。这很容易理解。

image-20220629140627320

image-20220629140923476

对于OOP:

image-20220629141011782

恰恰相反,添加一个变量只需要添加一个新的类(对象),但添加新的方法则需要修改已有的所有类(尽管可以使用继承或者一些设计模式来简化这一过程)。总之,也不难理解。

image-20220629141314504

当然,我们总是希望能简化这个过程,让我们在使用某种方式编写程序(FP或OOP)时也能够使用另一种方式的优势(FP擅长新操作/方法,OOP擅长新变量/类)

image-20220629141550416

最后是关于程序扩展性的一些编程哲学。

image-20220629142402786

Binary Methods with Functional Decomposition

假如有一个操作需要定义在众多参数(而不是某个单个变量调用)的基础上,例如二元操作

image-20220629183503961

例子:这里的矩阵有所不同,不是变量Variants和操作Operation的关系,而是两个变量之间的关系。

image-20220629183637692

ML的例子:因为要处理各个变体之间的关系,Add就不需要抛出异常

image-20220629184217741

我们的eval过程需要定义所有情况(函数分解),就像绿色矩阵中那样

image-20220629184340070

Double Dispatch

与上一节类似,本节要在Ruby中使用OOP的方式来实现类似的add_values操作,使用叫做双重分派的方式。

image-20220629184950399

同样的,先定义数据结构,如下,Rational也类似。

image-20220629185044174

image-20220629185244703

image-20220629185439100

我们在某个类中总是需要识别其他对象的类,所以需要调用v对象的方法,但类似上述例子不是面向对象的方式(显式的判断v的类型)。我们确实需要“告诉”v,self是什么类型,这一点使用动态分派可以做到,这个技巧称为double-dispatch(即分派两次)。

image-20220703130221200

这里直接参考代码:

每个类仍然需要定义三个方法(addInt、addRational、addString),核心思想就是在每个类的add_values中调用v的对应方法,由于类本身是确定的,所以也能确定v使用的对应方法,例如v.addInt self,至于v是什么子类调用哪个子类方法,由动态分派决定,这是第一次分派

然后v.addInt方法中的参数v就是之前的self,是确定的(从程序员角度),但程序调用v.i方法仍然需要动态分派确定哪个子类的方法,这是第二次分派

# Section 8: Binary Methods with OOP: Double Dispatch

# Note: If Exp and Value are empty classes, we do not need them in a
# dynamically typed language, but they help show the structure and they
# can be useful places for code that applies to multiple subclasses.

class Exp
  # could put default implementations or helper methods here
end

class Value < Exp
  # this is overkill here, but is useful if you have multiple kinds of
  # /values/ in your language that can share methods that do not make sense 
  # for non-value expressions
end

class Int < Value
  attr_reader :i
  def initialize i
    @i = i
  end
  def eval # no argument because no environment
    self
  end
  def toString
    @i.to_s
  end
  def hasZero
    i==0
  end
  def noNegConstants
    if i < 0
      Negate.new(Int.new(-i))
    else
      self
    end
  end
  # double-dispatch for adding values
  def add_values v # first dispatch
    v.addInt self
  end
  def addInt v # second dispatch: other is Int
    Int.new(v.i + i)
  end
  def addString v # second dispatch: other is MyString (notice order flipped)
    MyString.new(v.s + i.to_s)
  end
  def addRational v # second dispatch: other is MyRational
    MyRational.new(v.i+v.j*i,v.j)
  end
end

# new value classes -- avoiding name-conflict with built-in String, Rational
class MyString < Value
  attr_reader :s
  def initialize s
    @s = s
  end
  def eval
    self
  end
  def toString
    s
  end
  def hasZero
    false
  end
  def noNegConstants
    self
  end

  # double-dispatch for adding values
  def add_values v # first dispatch
    v.addString self
  end
  def addInt v # second dispatch: other is Int (notice order is flipped)
    MyString.new(v.i.to_s + s)
  end
  def addString v # second dispatch: other is MyString (notice order flipped)
    MyString.new(v.s + s)
  end
  def addRational v # second dispatch: other is MyRational (notice order flipped)
    MyString.new(v.i.to_s + "/" + v.j.to_s + s)
  end
end

class MyRational < Value
  attr_reader :i, :j
  def initialize(i,j)
    @i = i
    @j = j
  end
  def eval
    self
  end
  def toString
    i.to_s + "/" + j.to_s
  end
  def hasZero
    i==0
  end
  def noNegConstants
    if i < 0 && j < 0
      MyRational.new(-i,-j)
    elsif j < 0
      Negate.new(MyRational.new(i,-j))
    elsif i < 0
      Negate.new(MyRational.new(-i,j))
    else
      self
    end
  end

  # double-dispatch for adding values
  def add_values v # first dispatch
    v.addRational self
  end
  def addInt v # second dispatch
    v.addRational self  # reuse computation of commutative operation
  end
  def addString v # second dispatch: other is MyString (notice order flipped)
    MyString.new(v.s + i.to_s + "/" + j.to_s)
  end
  def addRational v # second dispatch: other is MyRational (notice order flipped)
    a,b,c,d = i,j,v.i,v.j
    MyRational.new(a*d+b*c,b*d)
  end
end

class Negate < Exp
  attr_reader :e
  def initialize e
    @e = e
  end
  def eval
    Int.new(-e.eval.i) # error if e.eval has no i method
  end
  def toString
    "-(" + e.toString + ")"
  end
  def hasZero
    e.hasZero
  end
  def noNegConstants
    Negate.new(e.noNegConstants)
  end
end

class Add < Exp
  attr_reader :e1, :e2
  def initialize(e1,e2)
    @e1 = e1
    @e2 = e2
  end
  def eval
    e1.eval.add_values e2.eval
  end
  def toString
    "(" + e1.toString + " + " + e2.toString + ")"
  end
  def hasZero
    e1.hasZero || e2.hasZero
  end
  def noNegConstants
    Add.new(e1.noNegConstants,e2.noNegConstants)
  end
end

class Mult < Exp
  attr_reader :e1, :e2
  def initialize(e1,e2)
    @e1 = e1
    @e2 = e2
  end
  def eval
    Int.new(e1.eval.i * e2.eval.i) # error if e1.eval or e2.eval has no i method
  end
  def toString
    "(" + e1.toString + " * " + e2.toString + ")"
  end
  def hasZero
    e1.hasZero || e2.hasZero
  end
  def noNegConstants
    Mult.new(e1.noNegConstants,e2.noNegConstants)
  end
end

对于操作Add而言,其eval方法需要首先调用自身e1和e2的eval(可能是Int等数据结构的eval也可能是操作Add的eval递归调用,动态分派,让e1、e2自己决定调用子类方法)。

image-20220703141619417

image-20220703142844797

对于静态类型语言例如Java,也同样适用双重分派,所以double-dispatch是一种实现OOP二元操作的重要方法。

image-20220703145109795

下面看看Java代码:

// Section 8: Binary Methods with OOP: Double Dispatch

abstract class Exp {
    abstract Value eval(); // no argument because no environment
    abstract String toStrng(); // renaming b/c toString in Object is public
    abstract boolean hasZero();
    abstract Exp noNegConstants();
}

abstract class Value extends Exp {
    abstract Value add_values(Value other); // first dispatch
    abstract Value addInt(Int other); // second dispatch
    abstract Value addString(MyString other); // second dispatch
    abstract Value addRational(Rational other); // second dispatch
}

class Int extends Value {
    public int i;
    Int(int i) {
	this.i = i;
    }
    Value eval() {
	return this;
    }
    String toStrng() {
	return "" + i;
    }
    boolean hasZero() {
	return i==0;
    }
    Exp noNegConstants() {
	if(i < 0)
	    return new Negate(new Int(-i));
	else
	    return this;
    }
    Value add_values(Value other) {
	return other.addInt(this);
    }
    Value addInt(Int other) {
	return new Int(other.i + i);
    }
    Value addString(MyString other) {
	return new MyString(other.s + i);
    }
    Value addRational(Rational other) {
	return new Rational(other.i+other.j*i,other.j);
    }
}

class MyString extends Value {
    public String s;
    MyString(String s) {
	this.s = s;
    }
    Value eval() {
	return this;
    }
    String toStrng() {
	return s;
    }
    boolean hasZero() {
	return false;
    }
    
    Exp noNegConstants() {
	return this;
    }

    Value add_values(Value other) {
	return other.addString(this);
    }
    Value addInt(Int other) {
	return new MyString("" + other.i + s);
    }
    Value addString(MyString other) {
	return new MyString(other.s + s);
    }
    Value addRational(Rational other) {
	return new MyString("" + other.i + "/" + other.j + s);
    }
}

class Rational extends Value {
    int i;
    int j;
    Rational(int i, int j) {
	this.i = i;
	this.j = j;
    }
    Value eval() {
	return this;
    }
    String toStrng() {
	return "" + i + "/" + j;
    }
    boolean hasZero() {
	return i==0;
    }
    Exp noNegConstants() {
	if(i < 0 && j < 0)
	    return new Rational(-i,-j);
	else if(j < 0)
	    return new Negate(new Rational(i,-j));
	else if(i < 0)
	    return new Negate(new Rational(-i,j));
	else
	    return this;
    }
    Value add_values(Value other) {
	return other.addRational(this);
    }
    Value addInt(Int other) {
	return other.addRational(this);	// reuse computation of commutative operation

    }
    Value addString(MyString other) {
	return new MyString(other.s + i + "/" + j);
    }
    Value addRational(Rational other) {
	int a = i;
	int b = j;
	int c = other.i;
	int d = other.j;
	return new Rational(a*d+b*c,b*d);
    }
}

class Negate extends Exp {
    public Exp e;
    Negate(Exp e) {
	this.e = e;
    }
    Value eval() {
	// we downcast from Exp to Int, which will raise a run-time error
	// if the subexpression does not evaluate to an Int
	return new Int(- ((Int)(e.eval())).i);
    }
    String toStrng() {
	return "-(" + e.toStrng() + ")";
    }
    boolean hasZero() {
	return e.hasZero();
    }
    Exp noNegConstants() {
	return new Negate(e.noNegConstants());
    }
}

class Add extends Exp {
    Exp e1;
    Exp e2;
    Add(Exp e1, Exp e2) {
	this.e1 = e1;
	this.e2 = e2;
    }
    Value eval() {
	return e1.eval().add_values(e2.eval());
    }
    String toStrng() {
	return "(" + e1.toStrng() + " + " + e2.toStrng() + ")";
    }
    boolean hasZero() {
	return e1.hasZero() || e2.hasZero();
    }
    Exp noNegConstants() {
	return new Add(e1.noNegConstants(), e2.noNegConstants());
    }
}

class Mult extends Exp {
    Exp e1;
    Exp e2;
    Mult(Exp e1, Exp e2) {
	this.e1 = e1;
	this.e2 = e2;
    }
    Value eval() {
	// we downcast from Exp to Int, which will raise a run-time error
	// if either subexpression does not evaluate to an Int
	return new Int(((Int)(e1.eval())).i * ((Int)(e2.eval())).i);
    }
    String toStrng() {
	return "(" + e1.toStrng() + " * " + e2.toStrng() + ")";
    }
    boolean hasZero() {
	return e1.hasZero() || e2.hasZero();
    }

    Exp noNegConstants() {
	return new Mult(e1.noNegConstants(), e2.noNegConstants());
    }
}   

Optional: Multimethods

使用multimethod可以避免使用double-dispatch来实现二元操作(本质上就是方法重载)

image-20220703173138973

使用重载的多个方法,但也存在缺点,容易造成方法调用的混淆。

image-20220703180957336

虽然通过不同子类的参数来重载方法在其他语言很常见,但在ruby中很难做到,主要有两点原因:

(1)首先ruby动态类型语言,没有对方法的参数添加类型限制

(2)其次ruby不允许除了覆写以外的同名函数(同名就意味着覆写override,而不是重载overload)

image-20220703181257430

但在其他静态的面向对象语言中,虽然提供了多种方法重载,但只是静态重载。在编写时需要指定静态类型(尽管运行时仍然动态分派)。但这就与我们课程例子所谓的二元操作关系不大了,因为课程中的两个操作对象,可能是Int、Rational或String,但显然在Java等语言中,这个操作对象的类型是确定的。

C# 4.0中加入了动态类型,因此也能够实现multimethod

image-20220703181903727

Multiple Inheritance

多继承,也是OOP老生常谈的话题。

image-20220703183106829

多继承的优势和劣势:

image-20220703210047422

继承结构间可能存在歧义:

image-20220703210951229

Mixins

mixins是指一个方法的合集,与类的区别在于没有实例。

ruby的modules就是mixins

image-20220704110453118

例子:

image-20220704140814535

image-20220704141004708

mixin的改变了方法的查找规则,先在类中寻找,然后在mixin中寻找,再在超类中寻找,再在超类的mixin中寻找,以此类推。

对于对象变量,mixin方法可能会造成问题:

image-20220704141020865

ruby中最有用的两个mixins,Comparable比较和Enumerable枚举。

其中>,<等比较运算符是定义在<=>之上的(即<,>,=等运算调用<=>比较)

其他的迭代器则是定义在each上

image-20220704144129232

image-20220704144305975

例子:

image-20220704144334798

image-20220704144636203

image-20220705151309948

image-20220705151632998

Interfaces

比较多继承与mixins ,和接口的区别

image-20220706095441591

接口的事情学过Java应该都很熟悉了

image-20220706095612243

image-20220706095737289

image-20220706095900016

接口可以与mixins共同使用,保证某些方法一定被类实现(例如Comparable需要实现<=>,Enumerable需要实现each)

image-20220706100607508

Optional: Abstract Methods

这一节是为了更详细的介绍OOP,所以介绍抽象方法,比如Java的抽象方法和C++的纯虚函数。

image-20220706100835054

一般会存在超类定义了某些子类必须覆写的方法,在Ruby中我们可以(例如下面的例子),我们可以不定义m2,却直接在m1中使用它,此时的m2就是子类必须定义的内容,并且我们不能单独A.new创建A的实例,否则会method-missing。

值得特别注意的是,在静态语言中是不允许这样做的,父类无法调用只在子类中声明定义的方法;ruby是可以在父类中直接调用只在子类声明定义的方法的,总的来说宽松一些(只有运行时才能判断该方法是否被父类或子类定义)。

image-20220706100924809

在静态类型语言中,type chcker会保证m2必须在父类中被声明定义,因此我们需要一些多余的语句(例如下面例子的raise语句部分),这样会非常冗余。

image-20220706101611711

因此有了抽象方法,只声明而不定义,留给子类(每一个子类必须)定义。这样同时也限制了父类的实例创建。

image-20220706101817431

OOP的代码传递与FP的代码传递对比:

OOP的方式:通过子类定义的m2传递到超类的m1中(即使超类m1定义时不会知道m2具体是什么内容,因为m2的定义是运行时才被动态分派的)

FP的方式:高等函数传递代码,高等函数也无法知晓具体内容(例如f中的g,虽然会根据语法认定为函数,但只有运行时才会知晓g具体是什么)。对于定义的f,存在caller(调用者例如h),调用者h提供g的定义,传递到被调用者f中。

总的来说,两者是类似的,都是在超类/callee中定义方法m,这个方法m中包含一些其他方法n(只有运行时才会知道定义的方法),而这个n的定义(额外信息/需要传递的代码)由子类或者caller调用者来提供。

这是一种常用编程手段。

image-20220706101951006

最后讨论没有接口的C++,接口可以通过类与全部抽象方法(纯虚函数)实现,然后接口实现通过类继承(多重继承)来实现。

image-20220706103632753
本章也是在介绍面向对象编程的同时对比了函数式编程,总的来说受益匪浅。

标签:PartC,end,Programming,return,Value,Languages,other,new,def
来源: https://www.cnblogs.com/ssjxx98/p/16694488.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有