0 0 投票数
评分

Programming Languages 的最后一次作业

这作业在网上的参考资料就比较少了,看来坚持到最后的人不多呀

作业的难点在于 Double-Dispatch,如果不能很好地理解,那么会极大加重代码量,还容易出错

首先要读懂 SML/NJ 的代码,然后完成缺少的部分

由于之前已经有过大量 SML/NJ 的训练,所以这里的依葫芦画瓢应该不难

接着是 Ruby 的风格,这是这一节的学习重点,在子类、父类、相似方法这样的设计上,SML 和 Ruby 采用了完全不同的设计思路,它们各自的优劣是教授反复提及的,也体现在了编程作业上

最难的部分是线段相交,我采用了分类讨论的方法,做到不重不漏,但在线段平行时直接把 SML 代码翻译成了 Ruby 代码,没有经过仔细推敲,应该是可以和之前的 4 类再合并一下,使整体更简化

以下代码获得 100 分并通过全部测试数据

(* University of Washington, Programming Languages, Homework 7, hw7.sml 
   (See also Ruby code.)
*)

(* Do not make changes to this code except where you see comments containing
   the word CHANGE. *)

(* expressions in a little language for 2D geometry objects
   values: points, lines, vertical lines, line segments
   other expressions: intersection of two expressions, lets, variables, 
                      (shifts added by you)
*)
datatype geom_exp = 
           NoPoints
	 | Point of real * real (* represents point (x,y) *)
	 | Line of real * real (* represents line (slope, intercept) *)
	 | VerticalLine of real (* x value *)
	 | LineSegment of real * real * real * real (* x1,y1 to x2,y2 *)
	 | Intersect of geom_exp * geom_exp (* intersection expression *)
	 | Let of string * geom_exp * geom_exp (* let s = e1 in e2 *)
	 | Var of string
(* CHANGE add shifts for expressions of the form Shift(deltaX, deltaY, exp *)
	 | Shift of real * real * geom_exp

exception BadProgram of string
exception Impossible of string

(* helper functions for comparing real numbers since rounding means
   we should never compare for equality *)

val epsilon = 0.00001

fun real_close (r1,r2) = 
    (Real.abs (r1 - r2)) < epsilon

(* notice curried *)
fun real_close_point (x1,y1) (x2,y2) = 
    real_close(x1,x2) andalso real_close(y1,y2)

(* helper function to return the Line or VerticalLine containing 
   points (x1,y1) and (x2,y2). Actually used only when intersecting 
   line segments, but might be generally useful *)
fun two_points_to_line (x1,y1,x2,y2) = 
    if real_close(x1,x2)
    then VerticalLine x1
    else
	let 
	    val m = (y2 - y1) / (x2 - x1)
	    val b = y1 - m * x1
	in
	    Line(m,b)
	end

(* helper function for interpreter: return value that is the intersection
   of the arguments: 25 cases because there are 5 kinds of values, but
   many cases can be combined, especially because intersection is commutative.
   Do *not* call this function with non-values (e.g., shifts or lets)
 *)
fun intersect (v1,v2) =
    case (v1,v2) of
	
       (NoPoints, _) => NoPoints (* 5 cases *)
     | (_, NoPoints) => NoPoints (* 4 additional cases *)

     | 	(Point p1, Point p2) => if real_close_point p1 p2
				then v1
				else NoPoints

      | (Point (x,y), Line (m,b)) => if real_close(y, m * x + b)
				     then v1
				     else NoPoints

      | (Point (x1,_), VerticalLine x2) => if real_close(x1,x2)
					   then v1
					   else NoPoints

      | (Point _, LineSegment seg) => intersect(v2,v1)

      | (Line _, Point _) => intersect(v2,v1)

      | (Line (m1,b1), Line (m2,b2)) =>
	if real_close(m1,m2) 
	then (if real_close(b1,b2)
	      then v1 (* same line *)
	      else  NoPoints) (* parallel lines do not intersect *)
	else 
	    let (* one-point intersection *)
		val x = (b2 - b1) / (m1 - m2)
		val y = m1 * x + b1
	    in
		Point (x,y)
	    end

      | (Line (m1,b1), VerticalLine x2) => Point(x2, m1 * x2 + b1)

      | (Line _, LineSegment _) => intersect(v2,v1)

      | (VerticalLine _, Point _) => intersect(v2,v1)
      | (VerticalLine _, Line _)  => intersect(v2,v1)

      | (VerticalLine x1, VerticalLine x2) =>
	if real_close(x1,x2)
	then v1 (* same line *)
	else NoPoints (* parallel *)

      | (VerticalLine _, LineSegment seg) => intersect(v2,v1)

      | (LineSegment seg, _) => 
	(* the hard case, actually 4 cases because v2 could be a point,
	   line, vertical line, or line segment *)
	(* First compute the intersection of (1) the line containing the segment 
           and (2) v2. Then use that result to compute what we need. *)
	(case intersect(two_points_to_line seg, v2) of
	    NoPoints => NoPoints 
	  | Point(x0,y0) => (* see if the point is within the segment bounds *)
	    (* assumes v1 was properly preprocessed *)
	    let 
		fun inbetween(v,end1,end2) =
		    (end1 - epsilon <= v andalso v <= end2 + epsilon)
		    orelse (end2 - epsilon <= v andalso v <= end1 + epsilon)
		val (x1,y1,x2,y2) = seg
	    in
		if inbetween(x0,x1,x2) andalso inbetween(y0,y1,y2)
		then Point(x0,y0)
		else NoPoints
	    end
	  | Line _ => v1 (* so segment seg is on line v2 *)
	  | VerticalLine _ => v1 (* so segment seg is on vertical-line v2 *)
	  | LineSegment seg2 => 
	    (* the hard case in the hard case: seg and seg2 are on the same
               line (or vertical line), but they could be (1) disjoint or 
               (2) overlapping or (3) one inside the other or (4) just touching.
	       And we treat vertical segments differently, so there are 4*2 cases.
	     *)
	    let
		val (x1start,y1start,x1end,y1end) = seg
		val (x2start,y2start,x2end,y2end) = seg2
	    in
		if real_close(x1start,x1end)
		then (* the segments are on a vertical line *)
		    (* let segment a start at or below start of segment b *)
		    let 
			val ((aXstart,aYstart,aXend,aYend),
			     (bXstart,bYstart,bXend,bYend)) = if y1start < y2start
							      then (seg,seg2)
							      else (seg2,seg)
		    in
			if real_close(aYend,bYstart)
			then Point (aXend,aYend) (* just touching *)
			else if aYend < bYstart
			then NoPoints (* disjoint *)
			else if aYend > bYend
			then LineSegment(bXstart,bYstart,bXend,bYend) (* b inside a *)
			else LineSegment(bXstart,bYstart,aXend,aYend) (* overlapping *)
		    end
		else (* the segments are on a (non-vertical) line *)
		    (* let segment a start at or to the left of start of segment b *)
		    let 
			val ((aXstart,aYstart,aXend,aYend),
			     (bXstart,bYstart,bXend,bYend)) = if x1start < x2start
							      then (seg,seg2)
							      else (seg2,seg)
		    in
			if real_close(aXend,bXstart)
			then Point (aXend,aYend) (* just touching *)
			else if aXend < bXstart
			then NoPoints (* disjoint *)
			else if aXend > bXend
			then LineSegment(bXstart,bYstart,bXend,bYend) (* b inside a *)
			else LineSegment(bXstart,bYstart,aXend,aYend) (* overlapping *)
		    end	
	    end						
	  | _ => raise Impossible "bad result from intersecting with a line")
      | _ => raise Impossible "bad call to intersect: only for shape values"

(* interpreter for our language: 
   * takes a geometry expression and returns a geometry value
   * for simplicity we have the top-level function take an environment,
     (which should be [] for the whole program
   * we assume the expression e has already been "preprocessed" as described
     in the homework assignment: 
         * line segments are not actually points (endpoints not real close)
         * lines segment have left (or, if vertical, bottom) coordinate first
*)

fun eval_prog (e,env) =
    case e of
	NoPoints => e (* first 5 cases are all values, so no computation *)
      | Point _  => e
      | Line _   => e
      | VerticalLine _ => e
      | LineSegment _  => e
      | Var s => 
	(case List.find (fn (s2,v) => s=s2) env of
	     NONE => raise BadProgram("var not found: " ^ s)
	   | SOME (_,v) => v)
      | Let(s,e1,e2) => eval_prog (e2, ((s, eval_prog(e1,env)) :: env))
      | Intersect(e1,e2) => intersect(eval_prog(e1,env), eval_prog(e2, env))
(* CHANGE: Add a case for Shift expressions *)
	  | Shift(dx, dy, e) => case eval_prog(e, env) of
      						NoPoints => NoPoints
      						  | Point(x, y) => Point(x + dx, y + dy)
      						  | Line(m, b) => Line(m, b + dy - m * dx) (* y = m * x + b, (y + dy) = m * (x + dx) + b' => b' = m * x + b + dy - m * x - m * dx = b + dy - m * dx *)
      						  | VerticalLine(x) => VerticalLine(x + dx)
      						  | LineSegment(x1, y1, x2, y2) => LineSegment(x1 + dx, y1 + dy, x2 + dx, y2 + dy)

(* CHANGE: Add function preprocess_prog of type geom_exp -> geom_exp *)
fun preprocess_prog e =
    case e of
    	LineSegment(x1, y1, x2, y2) =>
    		if real_close(x1, x2) then
    			if real_close(y1, y2) then
    				Point(x1, y1)
    			else
    				if y1 > y2 then
    					LineSegment(x2, y2, x1, y1)
    				else
    					e
    		else
    			if x1 > x2 then
    				LineSegment(x2, y2, x1, y1)
    			else
    				e
      | Let(s, e1, e2) => Let(s, preprocess_prog e1, preprocess_prog e2)
      | Intersect(e1, e2) => Intersect(preprocess_prog e1, preprocess_prog e2)
      | Shift(dx, dy, e) => Shift(dx, dy, preprocess_prog e)
      | _ => e
# University of Washington, Programming Languages, Homework 7, hw7.rb 
# (See also ML code)

# a little language for 2D geometry objects

# each subclass of GeometryExpression, including subclasses of GeometryValue,
#  needs to respond to messages preprocess_prog and eval_prog
#
# each subclass of GeometryValue additionally needs:
#   * shift
#   * intersect, which uses the double-dispatch pattern
#   * intersectPoint, intersectLine, and intersectVerticalLine for 
#       for being called by intersect of appropriate clases and doing
#       the correct intersection calculuation
#   * (We would need intersectNoPoints and intersectLineSegment, but these
#      are provided by GeometryValue and should not be overridden.)
#   *  intersectWithSegmentAsLineResult, which is used by 
#      intersectLineSegment as described in the assignment
#
# you can define other helper methods, but will not find much need to

# Note: geometry objects should be immutable: assign to fields only during
#       object construction

# Note: For eval_prog, represent environments as arrays of 2-element arrays
# as described in the assignment

class GeometryExpression  
  # do *not* change this class definition
  Epsilon = 0.00001
end

class GeometryValue 
  # do *not* change methods in this class definition
  # you can add methods if you wish

  private
  # some helper methods that may be generally useful
  def real_close(r1,r2) 
    (r1 - r2).abs < GeometryExpression::Epsilon
  end
  def real_close_point(x1,y1,x2,y2) 
    real_close(x1,x2) && real_close(y1,y2)
  end
  # two_points_to_line could return a Line or a VerticalLine
  def two_points_to_line(x1,y1,x2,y2) 
    if real_close(x1,x2)
      VerticalLine.new x1
    else
      m = (y2 - y1).to_f / (x2 - x1)
      b = y1 - m * x1
      Line.new(m,b)
    end
  end

  public
  # we put this in this class so all subclasses can inherit it:
  # the intersection of self with a NoPoints is a NoPoints object
  def intersectNoPoints np
    np # could also have NoPoints.new here instead
  end

  # we put this in this class so all subclasses can inhert it:
  # the intersection of self with a LineSegment is computed by
  # first intersecting with the line containing the segment and then
  # calling the result's intersectWithSegmentAsLineResult with the segment
  def intersectLineSegment seg
    line_result = intersect(two_points_to_line(seg.x1,seg.y1,seg.x2,seg.y2))
    line_result.intersectWithSegmentAsLineResult seg
  end

  # Move them to super class to simplify the code
  def eval_prog env 
    self # all values evaluate to self
  end
  def preprocess_prog
    self # no pre-processing to do here
  end
end

class NoPoints < GeometryValue
  # do *not* change this class definition: everything is done for you
  # (although this is the easiest class, it shows what methods every subclass
  # of geometry values needs)
  # However, you *may* move methods from here to a superclass if you wish to

  # Note: no initialize method only because there is nothing it needs to do

  def shift(dx,dy)
    self # shifting no-points is no-points
  end
  def intersect other
    other.intersectNoPoints self # will be NoPoints but follow double-dispatch
  end
  def intersectPoint p
    self # intersection with point and no-points is no-points
  end
  def intersectLine line
    self # intersection with line and no-points is no-points
  end
  def intersectVerticalLine vline
    self # intersection with line and no-points is no-points
  end
  # if self is the intersection of (1) some shape s and (2) 
  # the line containing seg, then we return the intersection of the 
  # shape s and the seg.  seg is an instance of LineSegment
  def intersectWithSegmentAsLineResult seg
    self
  end
end

class Point < GeometryValue
  # *add* methods to this class -- do *not* change given code and do not
  # override any methods

  # Note: You may want a private helper method like the local
  # helper function inbetween in the ML code
  attr_reader :x, :y
  def initialize(x,y)
    @x = x
    @y = y
  end

  def shift(dx, dy)
    Point.new(x + dx, y+ dy)
  end

  def intersect other
    other.intersectPoint self
  end

  def intersectPoint p
    if real_close_point(x, y, p.x, p.y)
      self
    else
      NoPoints.new
    end
  end

  def intersectLine l
    if real_close(y, l.m * x + l.b)
      self
    else
      NoPoints.new
    end
  end

  def intersectVerticalLine vl
    if real_close(x, vl.x)
      self
    else
      NoPoints.new
    end
  end

  def intersectWithSegmentAsLineResult seg
    seg.intersectPoint self
  end

end

class Line < GeometryValue
  # *add* methods to this class -- do *not* change given code and do not
  # override any methods
  attr_reader :m, :b 
  def initialize(m,b)
    @m = m
    @b = b
  end

  def shift(dx, dy)
    # (y + dy) = m * (x + dx) + b' -> b' = y + dy - m * x - m * dx = dy - m * dx + (y - m * x) = dy - m * dx + b 
    Line.new(m, b + dy - m * dx)
  end

  def intersect other
    other.intersectLine self
  end

  def intersectLine l
    if real_close(m, l.m)
      if real_close(b, l.b)
        self
      else
        NoPoints.new
      end
    else
      # y1 = m1 x + b1 = m2 x + b2
      x0 = (l.b - b) / (m - l.m)
      y0 = m * x0 + b
      Point.new(x0, y0)
    end
  end

  def intersectPoint p
    p.intersectLine self
  end
    
  def intersectVerticalLine vl
    Point.new(vl.x, m * vl.x + b)
  end

  def intersectWithSegmentAsLineResult seg
    seg.intersectLine self
  end
end

class VerticalLine < GeometryValue
  # *add* methods to this class -- do *not* change given code and do not
  # override any methods
  attr_reader :x
  def initialize x
    @x = x
  end

  def shift(dx, dy)
    VerticalLine.new(x + dx)
  end

  def intersect other
    other.intersectVerticalLine self
  end

  def intersectPoint p
    p.intersectVerticalLine self
  end

  def intersectLine l
    l.intersectVerticalLine self
  end

  def intersectVerticalLine v
    if real_close(x, v.x)
      self
    else
      NoPoints.new
    end
  end

  def intersectWithSegmentAsLineResult seg
    seg.intersectVerticalLine self
  end

end

class LineSegment < GeometryValue
  # *add* methods to this class -- do *not* change given code and do not
  # override any methods
  # Note: This is the most difficult class.  In the sample solution,
  #  preprocess_prog is about 15 lines long and 
  # intersectWithSegmentAsLineResult is about 40 lines long
  attr_reader :x1, :y1, :x2, :y2
  def initialize (x1,y1,x2,y2)
    @x1 = x1
    @y1 = y1
    @x2 = x2
    @y2 = y2
  end

  def preprocess_prog
    if real_close(x1, x2) && real_close(y1, y2)
      Point.new(x1, y1)
    elsif real_close(x1, x2) && y1 > y2
      LineSegment.new(x2, y2, x1, y1)
    elsif x1 > x2
      LineSegment.new(x2, y2, x1, y1)
    else
      self
    end      
  end
  
  def shift(dx, dy)
     LineSegment.new(x1 + dx, y1 + dy, x2 + dx, y2 + dy)
  end

  def intersect other
    other.intersectWithSegmentAsLineResult self
  end

  def inbetween (v, end1, end2)
    ((end1 - GeometryExpression::Epsilon <= v) && (v <= end2 + GeometryExpression::Epsilon)) || ((end2 - GeometryExpression::Epsilon <= v) && (v <= end1 + GeometryExpression::Epsilon))
  end

  def intersectPoint p
  	if real_close(x1, x2) && real_close(x1, p.x) && inbetween(p.y, y1, y2)
  	  Point.new(p.x, p.y)
    else
      m2 = (y2 - y1) / (x2 - x1)
      b2 = y2 - m2 * x2
      if real_close(p.x * m2 + b2, p.y)
		if inbetween(p.x, x1, x2) && inbetween(p.y, y1, y2)
      	  Point.new(p.x, p.y)
    	else
      	  NoPoints.new
      	end
      else
      	NoPoints.new
      end
    end   
  end
  
  def intersectLine l
    if real_close(x1, x2)
      Point.new(x1,  l.m * x1 + l.b)
    else
      m2 = (y2 - y1) / (x2 - x1)
      b2 = y2 - m2 * x2
      if real_close(l.m, m2)
        if real_close(l.b, b2)
          LineSegment.new(x1, y1, x2, y2)
        else
          NoPoints.new
        end
      else
        px = (b2 - l.b) / (l.m - m2)
        if inbetween(px, x1, x2)
          Point.new(px, px * l.m + l.b)
        else
          NoPoints.new
        end
      end
    end
  end

  def intersectVerticalLine vl
    if real_close(x1, x2)
      if real_close(x1, vl.x)
        self
      else
      	NoPoints.new
      end
    else
      if vl.x < x1 || vl.x > x2
      	NoPoints.new
      else
        Point.new(vl.x, y1 + (y2 - y1) * (vl.x - x1) / (x2 - x1))
      end
    end
  end

  # SEE HW7.SML FOR DETAILS
  def sameLine seg
  	if real_close(x1, x2) # the segments are on a vertical line
      # let segment a start at or below start of segment b
      if y1 < seg.y1
        aXstart, aYstart, aXend, aYend = [x1, y1, x2, y2]
        bXstart, bYstart, bXend, bYend = [seg.x1, seg.y1, seg.x2, seg.y2]
      else
        aXstart, aYstart, aXend, aYend = [seg.x1, seg.y1, seg.x2, seg.y2]
        bXstart, bYstart, bXend, bYend = [x1, y1, x2, y2]
      end
      if real_close(aYend, bYstart)
        Point.new(aXend, aYend) # just touching
      elsif aYend < bYstart
        NoPoints.new # disjoint
      elsif aYend > bYend
        LineSegment.new(bXstart, bYstart, bXend, bYend) # b inside a
      else
        LineSegment.new(bXstart, bYstart, aXend, aYend) # overlapping
      end
    else
      if x1 < seg.x1
        aXstart, aYstart, aXend, aYend = [x1, y1, x2, y2]
        bXstart, bYstart, bXend, bYend = [seg.x1, seg.y1, seg.x2, seg.y2]
      else
        aXstart, aYstart, aXend, aYend = [seg.x1, seg.y1, seg.x2, seg.y2]
        bXstart, bYstart, bXend, bYend = [x1, y1, x2, y2]
      end
      if real_close(aXend, bXstart)
        Point.new(aXend, aYend) # just touching
      elsif aXend < bXstart
        NoPoints.new # disjoint
      elsif aXend > bXend
        LineSegment.new(bXstart, bYstart, bXend, bYend) # b inside a
      else
        LineSegment.new(bXstart, bYstart, aXend, aYend) # overlapping
      end
    end
  end

  def intersectWithSegmentAsLineResult seg
  	if real_close(x1, x2) && real_close(seg.x1, seg.x2)
  	  sameLine(seg)
  	elsif real_close(x1, x2) && (!real_close(seg.x1, seg.x2))
  	  m2 = (seg.y2 - seg.y1) / (seg.x2 - seg.x1)
  	  b2 = seg.y1 - m2 * seg.x1
  	  xx = x1
  	  yy = xx * m2 + b2
  	  if inbetween(xx, seg.x1, seg.x2) && inbetween(yy, y1, y2) && inbetween(yy, seg.y1, seg.y2)
  	  	Point.new(xx, yy)
  	  else
  	    NoPoints.new
  	  end
  	elsif (!real_close(x1, x2)) && real_close(seg.x1, seg.x2)
  	  m1 = (y2 - y1) / (x2 - x1)
  	  b1 = y1 - m1 * x1
  	  xx = seg.x1
  	  yy = xx * m1 + b1
  	  if inbetween(xx, x1, x2) && inbetween(yy, y1, y2) && inbetween(yy, seg.y1, seg.y2)
  	  	Point.new(xx, yy)
  	  else
  	    NoPoints.new
  	  end
  	else
      m1 = (y2 - y1) / (x2 - x1)
  	  b1 = y1 - m1 * x1
  	  m2 = (seg.y2 - seg.y1) / (seg.x2 - seg.x1)
  	  b2 = seg.y1 - m2 * seg.x1
  	  if real_close(m1, m2)
  	    sameLine(seg)
  	  else
  	  	xx = (b2 - b1) / (m1 - m2)
  	  	yy = xx * m1 + b1
  	  	if inbetween(xx, x1, x2) && inbetween(xx, seg.x1, seg.x2) && inbetween(yy, y1, y2) && inbetween(yy, seg.y1, seg.y2)
  	  	  Point.new(xx, yy)
  	  	else
  	  	  NoPoints.new
  	  	end
  	  end
    end
  end
end

# Note: there is no need for getter methods for the non-value classes

class Intersect < GeometryExpression
  # *add* methods to this class -- do *not* change given code and do not
  # override any methods
  def initialize(e1,e2)
    @e1 = e1
    @e2 = e2
  end

  def preprocess_prog
    Intersect.new(@e1.preprocess_prog, @e2.preprocess_prog)
  end

  def eval_prog env
    @e1.eval_prog(env).intersect(@e2.eval_prog(env))
  end
end

class Let < GeometryExpression
  # *add* methods to this class -- do *not* change given code and do not
  # override any methods
  # Note: Look at Var to guide how you implement Let
  def initialize(s,e1,e2)
    @s = s
    @e1 = e1
    @e2 = e2
  end

  def preprocess_prog
    Let.new(@s, @e1.preprocess_prog, @e2.preprocess_prog)
  end
  
  def eval_prog env
    @e2.eval_prog([[@s, @e1.eval_prog(env)]] + env)
  end
end

class Var < GeometryExpression
  # *add* methods to this class -- do *not* change given code and do not
  # override any methods
  def initialize s
    @s = s
  end
  def eval_prog env # remember: do not change this method
    pr = env.assoc @s
    raise "undefined variable" if pr.nil?
    pr[1]
  end

  def preprocess_prog
    self
  end
end

class Shift < GeometryExpression
  # *add* methods to this class -- do *not* change given code and do not
  # override any methods
  def initialize(dx,dy,e)
    @dx = dx
    @dy = dy
    @e = e
  end

  def preprocess_prog
    Shift.new(@dx, @dy, @e.preprocess_prog)
  end

  def eval_prog env
    @e.eval_prog(env).shift(@dx, @dy)
  end
end
0 0 投票数
评分