#導覽列 #文章列表 #許洛豪
在光之翼的守護下訴說著一段段的英雄傳說。 [ 首頁 | 網誌 | 相簿 | 留言 | 訂閱 ]

A Wrapper of Android Google Maps API for Scala.

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

ScalaMap is a wrapper of Android Google Maps API for Scala.

Sorry for my poor English, but I think this may be useful if you wnat develop Android w/ Google Maps API application using Scala.

Overview

Currently, it’s not easy to use Android’s Google Maps API for two reasons.

  • There is a bug in Scala compiler. If a Java library reference to a static inner class in another Java library, the compiler will complain that "classfile is broken" (Scala 2.7.7) or "Missing dependency" (Scala 2.8).

    • Unfortunately, it is what happened in MapView, it return ViewGroup.LayoutParams in several public methods.
  • Scala cannot access Java protected static classes / fields / methods.

    • That is what happened in ItemizedOverlay, where you should call protected static method boundCenter(Drawable drawable) or boundCenterBottom (Drawable drawable) in subclass’s constructor.

To overcome this problem, I’ve created a simple wrapper library located at this GitHub repository. It’s not prefect and has a lot limitations , but I think it should work.

License

This library is release under GNU Lesser General Public License. See COPYING AND COPYING.LESSER for details.

Install

This wrapper library use SBT (Simple Build Tool) as build system, it is not required, but will make things easier. You should install it.

  1. Download and install SBT following this instruction, it should be easy.
  2. Download the source code of ScalaMap.
  3. Unzip it to a directory called scalamap/
  4. Copy your android.jar and maps.jar library files to scalamap/lib/
  5. $ cd scalamap/
  6. $ sbt package
  7. After step 7, you should get a JAR file called scalamap-0.0.1.jar under target/ directory.

USAGE

  • Remember adding scalamap-0.0.1.tar.gz to your classpath when you compile your project.

  • Take a look at the example code below and project under demo/ directory.

  • Import org.maidroid.scalamap._

  • Basically, you could use the following code to create a SMapView object, where R.id.mapview is pointed to a MapView object in layout file.

    • val sMapView = new SMapView (findViewById (R.id.mapview))
  • You should be able to use a SMapView object just like an ordinary MapView, but remember, it is a wrapper using composition instead of inheritance, so there are some methods is not implemented in this wrapper.

NOTE

  • If you need MapController, use sMapView.getSMapController() to get a SMapController wrapper object.

  • If you need hook listener to a MapView, use sMapView.getViewGroup() to get the MapView object as instance of ViewGroup.

  • DO NOT USE sMapView.getMapView() / s or you will get a compiler error message like "class is borken", YOU’VE BEEN WARNED.

  • There are two minor API difference:

    • The constructor of SItemizedOverlay is (Drawable drawalbe, int mode), where mode is one of BOUND_CENTER, BOUND_CENTER_BOOTOM, and NO_BOUND in SItemizedOverlay.
    • SMapView.setReticleDrawMode takes an arguement of type Int, which is one of the following three value: SMapView.DRAW_RETICLE_NEVER, SMapView.DRAW_RETICLE_OVER, and SMapView.DRAW_RETICLE_UNDER.

CODE EXAMPLE

This is the code example corresponding to the tutorial Hellp, MapView on Android Developer site.

package org.bone;

import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.View;
import android.widget.ZoomControls;
import android.widget.LinearLayout;

import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;
import com.google.android.maps.ItemizedOverlay;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.OverlayItem;
import com.google.android.maps.Overlay;

import org.maidroid.scalamap.SMapView

class HelloMap extends MapActivity
{
    private lazy val layout      = findViewById (R.id.zoomview).
                                   asInstanceOf[LinearLayout]
    private lazy val mapView     = new SMapView (findViewById(R.id.mapview))
    private lazy val mapOverlays = mapView.getOverlays
    private lazy val drawable    = getResources.
                                   getDrawable (R.drawable.androidmarker)
    private lazy val itemizedoverlay = new HelloItemizedOverlay (drawable)


    override def isRouteDisplayed = false
    override def onCreate(savedInstanceState: Bundle)
    {
        super.onCreate(savedInstanceState)
    
        setContentView(R.layout.main)

        mapView.setBuiltInZoomControls (true)

        val point1 = new GeoPoint (19240000, -99120000)
        val point2 = new GeoPoint(35410000, 139460000)

        val overlayitem1 = new OverlayItem (point1, "", "")
        val overlayitem2 = new OverlayItem (point2, "", "")

        itemizedoverlay.addOverlay (overlayitem1)
        itemizedoverlay.addOverlay (overlayitem2)

        mapOverlays.add (itemizedoverlay)
    }
}
package org.bone

import android.graphics.drawable.Drawable
import com.google.android.maps.OverlayItem
import java.util.ArrayList

import org.maidroid.scalamap.SItemizedOverlay
import SItemizedOverlay.BOUND_CENTER_BOTTOM

class HelloItemizedOverlay (drawable: Drawable) extends 
      SItemizedOverlay[OverlayItem] (drawable, BOUND_CENTER_BOTTOM)
{
    private lazy val mOverlays = new ArrayList[OverlayItem]

    override def createItem (i: Int) = mOverlays.get(i)
    override def size () = mOverlays.size() 

    def addOverlay (overlay: OverlayItem)
    {
        mOverlays.add (overlay);
        populate ();
    }
}
將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2010-01-14 (週四) 19:55:41

用 Scala 寫 Android 上的 Google Maps API 程式。

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

話說這兩天想到一些 Android 加 Google Maps 的應用,於是試著想用 Scala 試著做做看,沒想到竟然遇到了一堆問題,以致於陷入了苦戰之中。

簡單的來講,造成這次的狀況的原因,有以下兩個

  • Scala 的編譯器有蟲,當某個 Java 函式庫參照到另一個 Java 函式庫的 static inner class 時,就會找不到而爆炸。這次爆炸的原因就是 MapView 裡面用了 ViewGroup.LayoutParams。
  • Scala 因為語意上的關係,沒有 static 這種東西,以致於無法存取 static protected 的方法,所以 ItemizedOverlay 又不能用了。

結論:兩個最重要的東西都不能用啊!

為了解決這個問題,我只好自己寫一個 Wrapper,並且把他包成函式庫的形式,方便以後使用,或者以防有人遇到和我一樣的狀況卻求助無門,程式碼和說明在 GitHub 上

總之,目前總算是把 Hello, MapView 的範例跑起來了,程式碼如下,看起來和原來 Java 的版本差不多,這樣的 Wrapper 應該算還可以吧?

package org.bone;

import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.View;
import android.widget.ZoomControls;
import android.widget.LinearLayout;

import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;
import com.google.android.maps.ItemizedOverlay;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.OverlayItem;
import com.google.android.maps.Overlay;

import org.maidroid.scalamap.SMapView

class HelloMap extends MapActivity
{
    private lazy val layout      = findViewById (R.id.zoomview).
                                   asInstanceOf[LinearLayout]
    private lazy val mapView     = new SMapView (findViewById(R.id.mapview))
    private lazy val mapOverlays = mapView.getOverlays
    private lazy val drawable    = getResources.
                                   getDrawable (R.drawable.androidmarker)
    private lazy val itemizedoverlay = new HelloItemizedOverlay (drawable)


    override def isRouteDisplayed = false
    override def onCreate(savedInstanceState: Bundle)
    {
        super.onCreate(savedInstanceState)
    
        setContentView(R.layout.main)

        mapView.setBuiltInZoomControls (true)

        val point1 = new GeoPoint (19240000, -99120000)
        val point2 = new GeoPoint(35410000, 139460000)

        val overlayitem1 = new OverlayItem (point1, "", "")
        val overlayitem2 = new OverlayItem (point2, "", "")

        itemizedoverlay.addOverlay (overlayitem1)
        itemizedoverlay.addOverlay (overlayitem2)

        mapOverlays.add (itemizedoverlay)
    }
}
package org.bone

import android.graphics.drawable.Drawable
import com.google.android.maps.OverlayItem
import java.util.ArrayList

import org.maidroid.scalamap.SItemizedOverlay
import SItemizedOverlay.BOUND_CENTER_BOTTOM

class HelloItemizedOverlay (drawable: Drawable) extends 
      SItemizedOverlay[OverlayItem] (drawable, BOUND_CENTER_BOTTOM)
{
    private lazy val mOverlays = new ArrayList[OverlayItem]

    override def createItem (i: Int) = mOverlays.get(i)
    override def size () = mOverlays.size() 

    def addOverlay (overlay: OverlayItem)
    {
        mOverlays.add (overlay);
        populate ();
    }
}
將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2010-01-13 (週三) 22:35:29

Recovery From Addiction,加上 Ruby / Scala。

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

嗯,剛好在『PHP 很爛』這篇文章裡看到了這個 Recovery From Addiction 的影片。

基本上關於 PHP 爛不爛的部份每個人都有想法,但基本上我是論認為 PHP 很爛的那一派,我的碩士論文是實作 Drupal 的一個模組,用的是 PHP,可是我還是覺得 PHP 很爛。

但不可否認的是,在我學 Scala 之前,要做用了就丟的東西時候我的第一個選擇會是 PHP(因為我沒學 shell scrip),簡單,好用,用了就丟的東西也不用講究什麼安全性或強固性,可以動就好了。

但自從學了 Scala 之後,Scala 會是我的首選,再不然,我也會選 Ruby,雖然我的 Ruby 程度大概還只停留在 Hello World 而已。

回到正題,Recovery From Addiction 是一個很棒的影片,不過可惜的是裡面的例子只有提到 Java vs Python 而已,但是個人私心很喜歡 Scala 還有 Ruby ,所以就在這裡補上 Ruby 和 Scala 例子。

想從 Java 跳船的人可以參考一下 Recovery From Addiction 影片,以及這些例子。

雖然影片是從座標的函式庫的設計開始講,不過這裡會從客戶端開始說。

我們先看 Java / Python / Ruby / Scala 三者的客戶端程式。

// This is Java.
Coord coord = new Coord ();
coord.lat = -93.0 // Souther than south!
# This is Python.
coord = Coord ()
coord.lat = -93.9 # Souther than south!
# This is Ruby.
coord = Coord.new
coord.lat = -93.9 # Souther than south!
// This is Scala.
val coord = new Coord ()
coord.lat = -93.0F  // Souther than south!

基本上都是一樣的,產生一個 Coord 物件,然後指定一個座標。接下來,我們來看第一版的函式庫端。

// This is Java.
public class Coord {
    public float lat;
    public float lon;

    public Coord () {}
    public Coord (float lat, float lon) {
        this.lat = lat;
        this.lon = lon;
    }
}
# This is Python.
class Coord (object)
    def __init__ (self, lat = 0.0, lon = 0.0):
        self.lat, self.lon = lat, lon
# This is Ruby.
class Coord
    attr_accessor :lat, :lon
    def initialize(lat = 0.0, lon = 0.0)
        @lat = lat
        @lon = lon
    end
end
// This is Scala.
class Coord (var lat: Float, var lon: Float) {
    def this () = this (0, 0)
}

到這邊為止,Python / Ruby / Scala 還是不算完全打敗 Java,畢竟 Java 也才九行程式碼,Ruby 也要七行。

重點是下面了,現在我們發現原來的設計有問題,因為 lat 不能小於 -90 或是大於 90 ,我們要如何在不更動原有的客戶端的情況下,把這個判斷加到我們的函式庫呢?Python / Ruby / Scala 都可以很輕鬆的做到,但 Java 就不行了。

先看 Python 的:

class Coord (object)
    def __init__ (self, lat = 0.0, lon = 0.0):
        self.lat, self.lon = lat, lon

    def getLat (slef):
        return self.__lat

    def setLat (self, lat):
        if not -90.0 <= lat <= 90.0:
            raise ValueError ("Bad latitude")
        self.__lat = lat
      
    lat = property (getLat, setLat)

這段程式碼裡面,我們設了 getLat 和 setLat 這兩個函式,接著再指定說 lat 的 getter / setter 就分別是 getLat 和 setLat。

這個則是 Ruby 的:

class Coord
    attr_accessor :lon
    attr_reader   :lat
    def initialize(lat = 0.0, lon = 0.0)
        @lat = lat
        @lon = lon
    end

    def lat=(lat)
        if (lat < -90 || lat > 90)
            raise "Bad latitude" 
        end

        @lat = lat
    end
end

在 Ruby 裡的做法,則是叫 Ruby 自己生一個 getter 給我們,然後我們再定義 lat=(lat) 這個函數,而這個函數就是 lat 的 setter。

接著看 Scala 的做法:

class Coord (var mLat: Float, var lon: Float) {
    def this () = this (0, 0)

    def lat = mLat
    def lat_= (lat: Float) {
        if ( lat < -90 || lat > 90 )
            throw new Exception ("Bad latitude")

        mLat = lat
    }
}

在 Scala 裡,我們也是自己定義了 lat 的 getter / setter,眼尖的朋友可能發現了原來成員變數的 lat 現在變成了 mLat,這是 Scala 本身的限制,因為 Scala 會幫我們產生一組 getter / setter,所以為了必免命名衝突,得用另一個名稱。

所幸的是,Scala 是靜態語言,所以你大可先改成員變數的名稱,然後叫編譯器幫你抓出所有錯的地方,基本上很簡單就可以做程式碼的重構,很少遇到什麼大問題。

另外,Scala 神奇的地方,就在於雖然他寫起來向 Ruby / Python 這種動態型別的程式語言,可是實際上他是靜態型別的,所有的型別錯誤在編譯時期就會被抓出來。

我完全不會 Python、Ruby 是初學、Scala 是漸漸上手,在這邊野人獻曝一下,分享一下 Ruby 和 Scala 的程式碼。

順道一提,影片中有提到 Ruby 的 Unicode 問題,但如果我記得沒錯的話,目前 Ruby 應該是支援 Unicode 了。

然後,雖然作者有說他不喜歡 Ruby 那些從 Perl 借來,看起來很像暗碼的變數符號,但我自己就整個程式的觀點來看,其實我發現只要掌握了一些大的原則,Ruby 的程式碼比 Python 更容易看懂。

舉一個實際一點的例子,我就看不出來為什麼 Python 裡成員函數要存取成員變數的時候要用 self.__lat 這個方式,不清楚為什麼 lat 前面還要多兩個底線。

相較之下,Ruby 只是用了變數開頭的符號來區別是哪種變數罷了,就程式的閱讀和撰寫來說,和其他語言感覺沒有太大的隔閡。

當然,這只能說是個人的喜好就是了,畢竟 Python / Ruby 兩者本身所信仰的 coding 哲學本來就不同,只能說我比較偏好 Scala / Ruby 這邊的設計哲學而已。

最後的最後,其實我很訝異最後 Scala 的版本竟然會比 Python / Ruby 還來得少行,再加上還是靜態型別,可見 Scala 狂勝 Python / Ruby !(大誤)

PS、程式語言比較的東西,看看就好,用自己順手,喜歡的工具最重要!

將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2010-01-07 (週四) 23:16:12

[Android] 終於從無間 ScrollView / TextView 地獄回來了!

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

這是接續之前寫的:『可以 click 的 ScrollView』,因為之前的版本是有問題的!

說實話,這是寫 Android 寫到現在被搞最久的一次了。之前大部份都還滿順手的,遇到的問題 Google 一下也大部份都有人問過,或是提供了解答的線索。

但這次就不同了,被搞了超久,追了半天才找到問題所在,終於做出我想要的東西出來了。

簡單來講,為了要實作出理想的 Maidroid 系列,我必須要有一個元件是用來當做對話框的,就像 AVG 遊戲裡的對話框一樣,是用來讓程式裡的妺斗們與主人互動的。

在實作上來講,就是要有一個區塊能夠顯示對話,而當使用者按下去時,能夠顯示下一句的對話。

看起來好像很簡單,用 TextView 就可以做到了,但實際上還有另外一樣需求,就是當文字超出範圍時,要能夠出現捲軸,讓使用者捲動。

合理的選擇是用 ScrollView 把 TextView 包起來。因為如果單純地使用TextView 的話,會有很麻煩的效果出現,而且我不知道為什麼,雖然可以捲動,但 Scroll Bar 就硬是不給我出現。

但這一包,問題就一大堆了。

包話 View.OnClickListener 完全不能用,所以必需自己用其他方式來判斷是否是 Click 的動作。

本來以為用 OnTouchEventListener 就好了,沒想到用下去,只有當對話框出現捲軸時才有用,追了好幾天,試了好幾種方法,才終於找到一個非常不起眼的原因--在 ScrollView 的實作裡頭,如果這個 View 沒有出現捲軸,他會把除了 ACTION_DOWN 之外的 MotionEvent 全部給吃光光!

就為了這個奇怪的行為,浪費了我好幾天啊!我可以理解如果沒有捲軸你就不需要處理,可是也沒必要把 Event 吃掉吧!?(淚)

所以,以下終於是正確可動的版本了,另附轉換特效,以及給其他人在最後一個訊息時可以做出反應的 Inject Method。

順道一題,這是 Scala 的版本,Java 的話請自行轉換吧,反正整個流程和概念是一樣的。

不過話說回來,我還是不確定這是不是最恰當的實作方式就是了(特別是在是否為 Click 的判斷上),但至少它可以動。XD

也就是說,停滯了這麼多天,我終於可以繼續進行下一步了。orz…

import android.content.Context

import android.view.animation.Animation
import android.view.animation.Animation.AnimationListener
import android.view.animation.AnimationUtils._

import android.view.View
import android.view.MotionEvent

import android.widget.ScrollView
import android.widget.TextView

import android.util.AttributeSet
import android.util.Log

class ViewMessageBox (context: Context, attrs: AttributeSet) extends 
      ScrollView     (context, attrs)
{
    // Call Initialzation when construct
    init () 

    /*=================================================================
     * Private Fields
     *===============================================================*/
    private lazy val MAX_OFFSET = 1
    private lazy val textView = new TextView (context)
    private var      messageList: List[String] = Nil

    /*=================================================================
     * Initialization Routine
     *===============================================================*/
    private def init ()
    {
        textView.setTextSize (20)
        addView (textView)
        
        this.setOnTouchListener (onTouchListener)
    }

    /*=================================================================
     * Private Methods
     *===============================================================*/

    /**
     *  用來檢查是否有 ScrollBar 出現,測試的標準是當 TextView 比
     *  ScrollView 來的大時,就當做有 ScrollBar。
     */
    private def hasScrollBar = textView.getHeight > this.getHeight

    private def switchToMessage (message: String, next: List[String])
    {
        def updateTextView (message: String)
        {
            val ARROW     = "\u25bc"
            val DIAMOND   = "\u25c6"
            val endSymbol = if (next == Nil) DIAMOND else ARROW

            textView.setText (message + endSymbol)
        }

        def prepareAnimation (): Animation =
        {
            val fadeIn  = loadAnimation (context, android.R.anim.fade_in)
            val fadeOut = loadAnimation (context, android.R.anim.fade_out)

            fadeOut.setAnimationListener (new AnimationListener () {
                def onAnimationEnd (animation: Animation) {
                    updateTextView (message)
                    textView.startAnimation (fadeIn)
                }

                def onAnimationRepeat (animation: Animation) {}
                def onAnimationStart  (animation: Animation) {}
            })

            fadeOut
        }

        val animation = prepareAnimation ()
        textView.startAnimation (animation)
    }

    /*=================================================================
     * Callbacks
     *===============================================================*/

    /**
     *  重點在這裡。
     *
     *  Click 的定義:壓下與放開的點誤差不超過 1
     */
    private lazy val onTouchListener = new View.OnTouchListener ()
    {
        // 壓下起始座標
        var startX = 0.0
        var startY = 0.0

        def onTouch (view: View, event: MotionEvent): Boolean = {

            // 如果沒有 ScrollBar 出現,除了 ACTION_DOWN 之外
            // 的 Event 都會被吃掉,所以直接當做 Click 處理。
            if (!hasScrollBar) {
                showNextMessage ()
                return false
            }

            // 如果有 ScrollBar,先記錄起始下壓點在哪,如果按下
            // 與放下的點 X 與 Y 軸均不超過 MAX_OFFSET ,就當做
            // 是 Click 事件。
            event.getAction match {
                case MotionEvent.ACTION_DOWN =>
                    startX = event.getX
                    startY = event.getY

                case MotionEvent.ACTION_UP =>
                    if (Math.abs(startX - event.getX) < MAX_OFFSET &&
                        Math.abs(startY - event.getY) < MAX_OFFSET) {
                        showNextMessage ()
                    }

                case _ =>
           }

           return false
        }
    }

    /*=================================================================
     * Public Field / Method
     *===============================================================*/

    // 當最後一個訊息被顯示時會呼叫這個函式
    var injectWhenLast: () => Unit = () => {}

    def setMessageList (message:List[String])
    {
        messageList = message
        showNextMessage ()
    }

    def showNextMessage ()
    {
        // 將 TextView 設成 List 裡的第一個字串
        messageList match {
            case message :: next =>  
                switchToMessage (message, next)

                if (next == Nil) {
                    injectWhenLast ()
                }

                messageList = next

            case _ =>
        }
    }
}
將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-12-18 (週五) 19:37:50

[Android] 可以 click 的 ScrollView。

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

話說 Android 裡的 ScrollView 是不能 click 的,但為了模擬出 AVG 裡的對話框效果,又想要讓文字超過對話框大小時自動出現捲軸功能,於是就用下面的奇技淫巧做了一個可以 click 的 ScrollView。

然後在這裡 click 的定義是按下的點與最後放開的點,X/Y 軸的距離各不超過 MAX_OFFSET ,至於這個值要用什麼比較好,目前還在測試。

import android.content.Context
import android.util.AttributeSet

import android.widget.ScrollView
import android.widget.TextView

import android.view.View
import android.view.MotionEvent

import android.util.Log

class ViewMessageBox (context: Context, attrs: AttributeSet) extends 
      ScrollView     (context, attrs)
{
    init ()

    private lazy val textView = new TextView (context)
    private val MAX_OFFSET = 1

    private def init ()
    {
        textView.setText ("Hello World")
        textView.setTextSize (20)

        addView (textView)

        this.setClickable (true)
        this.setFocusable (true)

        this.setOnTouchListener (onTouchListener)
    }

    /**
     *  重點在這裡。
     *
     *  Click 的定義:壓下與放開的點誤差不超過 1
     */
    private lazy val onTouchListener = new View.OnTouchListener ()
    {
        // 壓下起始座標
        var startX = 0.0
        var startY = 0.0
        
        def onTouch (view: View, event: MotionEvent) = {
            event.getAction match {
                case MotionEvent.ACTION_DOWN =>
                    startX = event.getX
                    startY = event.getY
        
                case MotionEvent.ACTION_UP =>
                    if (Math.abs(startX - event.getX) < MAX_OFFSET &&
                        Math.abs(startY - event.getY) < MAX_OFFSET) {
                        Log.e ("qqq", "Clicked[")
                    }
        
                case _ =>
           }
           
           false
        }
    }

}
將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-12-17 (週四) 09:08:55

懷念 Ruby 隨便給你放的陣列嗎?咱家的 Scala 也有!

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

好吧,雖然是很簡單的一件事,但我不知道為什麼我看的 Scala 的書和文件裡都沒有提到這些事,或許這可能不是很重要,又或者是明顯到根本不需要特別提?

不過,我還是寫一下好了……

話說像 Ruby 這類動態型別程式語言,大部份都有一個有趣的特性,那就是陣列裡面隨便你放任何東西。

舉例來說,下面的陣列在 Ruby 裡是合法的:

arr = [1, 3.14159, "我是字串", Time.now]

然而,像在 C / C++ / Java 這類靜態型別程式語言當中,陣列通常會與型別掛勾,陣列裡只能放相同型別的東西,例如下面的陣列,就只能放整數了。

int x [] = new int[10]  // 我只能放整數

這造成了不小的限制,但幸好在 Java 中,由於所有物件都是 Object 的子類別,所以你還是可以像這樣繞道而行,只是程式碼會變很醜而已,而且還得自己想辦法轉型。

// 我能放任何東西,可是請你自己轉型,順道一提,出了錯我不賠錢的
Object x [] = new Object[10]

而在 Scala 之中,一切都變得很簡單了,你有兩個選擇--Structural Type 與 Pattern Matching。

Structural Type 的想法其實就是 Duck Type 的另一種說法罷了--如果他叫起來像鴨子,走起路來也像鴨子,那麼他就是鴨子。

簡單來講,就是我只關心他有沒有實作某個方法或屬性,管他這個方法是哪裡來的,管他是繼承哪個類別或介面,只要他有這個方法或屬性就可以囉!

這麼一來,我就可以宣告一個陣列,裡面放的都是具有 myPrint() 這個方法的物件,但彼此並不互相繼承,也沒有共同的父類別或介面。

這麼一來,我就擁有一個比 C/C++/Java 彈性一點,又比 Ruby 安全一點的陣列(Ruby 可不可以有這樣的限制我不知道,煩請熟悉 Ruby 的朋友幫忙補完),因為我確定我在呼叫陣列裡的每個元素的 myPrint() 方法的時候,絕對不會出錯,一定找得到這個方法。

至於 Pattern Matching 的部份,其實和 Object [] 的概念是一樣的,只是語法簡潔很多,而且在進行型別比對時就幫你轉型,所以你可以很放心不會不小心寫錯。另外,程式碼會漂亮很多,從此脫離 if/else 的魔掌了(大誤)!

以下就是一些實際的程式碼啦,基本上應該有點 Java + Ruby 的基礎就看得懂了,主要的重點大概就是:

  • Any 相當於 Java 與 Ruby 中的 Object,是一切物件的父類別。
  • [T] 是 Generic Type,把他想成 Java 裡的 <> 就好了,例如 List[T] 就是 List<T>。
  • list1.foreach () 就等於 Ruby 的 list.each {|x| ….},只是 x 的部份直接省略為 _ 。
  • match 相當於 Java 的 switch,只是他可以連型別一起比對,並且直接幫你轉型,所以當 case x: ForInt 為真時,x 會轉型成 ForInt 。
/***************************************************************/
/* 簡單定義一些沒有共同父類別的東西                            */
/***************************************************************/

case class ForInt (x: Int) {
    def myPrint () = println ("MyPrint:" + x)
}

case class ForDouble (x: Double) {
    def myPrint () = println ("MyPrint:" + x)
}

case class ForString (x: String) {
    def myPrint () = println ("MyPrint:" + x)
}

/***************************************************************/
/* 利用 Strucural Type 來宣告 List                             */
/***************************************************************/

// 注意以下三個並沒有共通的父類別
val forInt:    ForInt    = new ForInt (3)
val forString: ForString = new ForString ("Hello World")
val forDouble: ForDouble = new ForDouble (3.14159)

// 這個 List 可以放入任何具有 myPrint(): Unit 方法的物件,不論他
// 們是否有共同的父類別,或實作相同的介面。
//
// 第二個 List[T] 是必要的,不然 Type Inference 機制會分析錯誤,
// 造成型別不符的錯誤。
type T = {def myPrint(): Unit}
val list1: List[T] = List[T] (forInt, forString, forDouble)

// 依序呼叫 List 裡每個元素的 myPrint() 方法
list1.foreach (_.myPrint()) 

/***************************************************************/
/* 以 Pattern Matching 實作隨便你放任何東西的 List             */
/* 於執行期再依物件類別決定要做啥                              */
/***************************************************************/

val list2: List[Any] = List (1, "我是字串", 3.45, new ForInt(3))
list2.foreach ( _ match {
    case x: Int    => println ("我是 Int:" + x)
    case x: String => println ("我是字串:" + x)
    case x: ForInt => println ("我是 ForInt:" + x)
    case x         => println ("我是其他東西:" + x)
})
將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-12-16 (週三) 11:59:48

CWT23 與 APH 的 Scala DSL。

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...
灣娘卡灣娘卡

警告:本文內含 APH  同人與技術混合內容,APH 為一國家擬人化創作,內容不代表真實國家情形及立場。

話說今天去了 CWT23  的攤位,結果都沒看到我原本萌的東西啊,然後在會場被萌到的大概就是灣娘吧,滿滿的灣娘在路上走來走去,真是萌煞人也。

於是,兩天的購物內容就都變成和灣娘有關的東西是也!而今天入手的就是這套 APH  紙牌遊戲,購買的原因……有阿呆毛的灣娘好萌(大誤)!

好吧……回到正題,其實買的時候很掙扎的,僅管我承認我是個阿宅,可是要在會場上像斑目那樣子的買法,我還是沒辦法的啊,更何況我還是個米蟲,還在等上工呢。

所以,在會場的時候,一直在和另一攤的萌獸DNA與戀星這兩套自製遊戲的合輯考慮,而最後還是選了灣娘卡。

理由嘛,其實是因為我覺得這種卡片遊戲好像還滿適合做成手機連線對戰遊戲的,所以打算買回來研究一下!

然後呢……回家後就不小心搞出這個 DSL  了,花費時間,好像十分鐘不到吧!結論就是 Scala  好強好厲害,最後一段設定遊戲的部份,根本就不像程式語言,可讀性莫名其妙的強,搞不好一般人都看得懂。XD

/**
 *  人物卡片擁有三個屬性:名稱、生命值、描述
 */
case class PersonCard (name: String, description: String, hp: Int) 
{
    def hasHP      (hp: Int) = PersonCard (name, description, hp)
    def describeAs (description: String) = 
        PersonCard (name, description, hp)
}

/**
 *  每個遊戲裡有複數張人物卡片
 */
case class GameBoard (title: String, 
                      personCards: List[PersonCard]) 
{
    def hasPersonCards (cards: PersonCard*) = 
        GameBoard (title, cards.toList)
}

/**
 *  神奇的轉型函式,DSL 的秘密就在這裡
 */
implicit def str2PersonCard (name: String) = 
    PersonCard (name, "", 0)

implicit def str2GameBoard (title: String) = 
    GameBoard (title, Nil)

/**
 *  今天在 CWT23 買的 APH 同人遊戲裡的一些國家卡片
 */
"APH 紙上遊戲" hasPersonCards (

    "台灣" hasHP 60  describeAs "一般來說,中國、香港、日本、韓國、台灣" +
                                "裡,台灣最好相處(因為太弱啦),雖然自" +
                                "己不覺得,但是大家都覺得她家的夜市很好" +
                                "玩。",

    "香港" hasHP 70  describeAs "被英國領養了一段時間,最近回大哥家住," +
                                "有點適應不良,對金錢很精明,但人際關係" +
                                "則有點無力,看不大出來他在想什麼。",

    "美國" hasHP 100 describeAs "熱血青年,自認是世界的警察,對於做家務" +
                                "有點懶洋洋,不過墨西哥會從後院的洞偷溜" +
                                "進來工作所以沒關係,是最近一次大流行感" +
                                "冒的病源。"
)
將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-12-13 (週日) 19:19:41

我還是覺得這是基礎中的基礎。

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

算是 BBS 上的舊文重 PO 吧,畢竟這是自己的一些想法,留在部落格上好像比較好一點。

原先這篇文章是回應 aMaa 網友的這一篇『類別的方法中為什麼可以建立本身類別的物件』的問題,看起來好像沒啥關聯,但我覺得這還是很重要的,至於後續還有一些發展以及想法,我會另外用專文來寫。

簡單的講,我還是覺得這些看似不相干的基礎是很重要的。

當然,如果你寫程式的目的只是為了好玩,只是想做玩具,只想當成手邊解決問題的工具,那麼這些確實可以不用去弄懂它。

但如果你想靠這行吃飯,我覺得還是認命一點,把基礎搞好吧……

就像,你可以拿起鎚子、釘子、木板自己蓋出一間狗屋,但你應該不會想要去住沒有建築基礎專業知識的設計師、建築師蓋出來的房子吧?


【離題】

說實話,這個討論串讓我想到之前在自己的部落格上寫到的這一篇雜記裡關於程式語言學習的部份,有問到到底在學習程式式語言的時候,要從何處入手。

這一串討論看下來,看來看去,只有一個想法:會有這種疑問,根本就是因為不了解整個 von Neumann架構/程式語言/編譯器/機器語言/虛擬機器之間的關連所造成的嘛……

以下,可能很長,可能很多看似無關緊要的東西(就算你不懂,你的程式還是可以動),可是我自己認為這是值得去了解的基礎,如果你真的用心了解下面所講的東西,基本上就不會再出現類似的疑惑了。

【回題】

其實原 PO 的問題本質上和另一個問題一樣:為什麼像是下面的遞迴程式裡,第 6 行的時候,明明 sum 還明有定義完,卻可以呼叫自己呢?

// 用遞迴計算 1 + 2 + ... + n
int sum (int n) {
   if (n == 1) {
       return 1;
   }

   return n + sum(n-1);
}

為什麼下面的程式裡,Node 明明還沒定義完,裡頭卻又出現另一個 Node 呢?

class Node {
   private Node next;
}

我真的很想大叫:不要鬧了!大家到底知不知道 Java 程式語言 / Bytecode / Virtual Machine / von Neumann 電腦架構 / 原生可執行檔 / 機器語語之間的關係,到底知不知道一個 Java 程式是怎麼執行的啊?!

請問你自己一個問題:Java 程式執行的時候,是執行你寫的原始碼嗎?

(答:不是,實際上執行的是 bytecode,也就是 javac 翻譯出來的 .class 檔)

請問你自己第二個問題:你在執行你的 Java 程式 (Bytecode) 的時候,你知道 Bytecode 到底是什麼東西,做什麼用的嗎?

(答:可以將 Bytecode 視為一套虛擬的『類機器語言』,將其交由 Virtual Machine 解譯後,可以產生電腦 CPU 真正能夠理解的機器語言指令)

請問你自己第三個問題:你知道什麼叫 von Neumann 架構嗎?你知道一個『原生電腦程式』是如何在 von Neumann 架構上執行的嗎?

(答:如果不知道,請參閱『小人電腦』,裡面是簡化版的說明,但基本上目前的所有電腦都不脫離這個架構。)

如果你看完了上面的小人電腦,請你問你自己第四個問題:你在小人電腦裡,有看到『資料結構』、『函數』、『物件』、『類別』這種東西嗎?

(答:沒有,只有記憶體/指令/暫存器/Program Counter ,而且記憶體位置好像也都知能存數字【註】)

(謎之音:那我的函數、物件、資料夾構在哪裡?提示:我們還有可執行檔以及 Bytecode 這兩個東西沒講到。)

【註】嚴格來說這並不正確

請再問你自己第五個問題:你知道我們剛剛『定義』出來的計算總合的函式,如果翻譯成小人電腦的機器語言,用上述網頁中的機器語言表示出來會長什麼樣子嗎?

(答:你會看到 sum(n-1) 被翻成一個 CALL 指令,其參數 XX 的部份會是此函數的開頭,而 return 會被翻成 RETURN ,如果你不知道這是什麼意思,請把小人電腦再認真看一次,並注意最後一個函數呼叫的例子)

以上,是回答為什麼函數可以呼叫自己。

接下來,請再問你自己:記憶體 / 指標 / 參考 / 物件之間,究竟是什麼關係,你知道第二個 Node 的 Java 程式碼,被載入到記憶體後,到底長什麼模樣嗎?

(答:指標和參考本質上都是相同的,都是『記憶體位置』)

這不就很明顯了嗎?private Node next,說的是『next 是一個記憶體位置,而且這個記憶體位置應該要指到一個長得像 Node 的物件』。

但 next 真的必需指到 Node 物件嗎?(提示:多型/強制轉型/執行期錯誤)他不過就是個數字,來表示記憶體位置,誰管他指到什麼地方啊!

所以說,這有什麼好訝異的?不過就是『在 Node 的這個物件裡,有一個欄位是一個數字,他指到某一個記憶體的位置,而這個記憶體位址上的內容,應該要是另一個Node 物件(但不必然是)。』

這很直覺,很正常啊!

最後,回到原 PO 的問題,如果再把原 PO 的問題更簡化,可以問另一個問題,那就是以下的 Java 程式碼是否合法,如果它合法的話,執行這段 Java 程式碼裡的 test() 到底會發生什麼事,記憶體裡產生了哪些變化?

class Hello
{
    public void test ()
    {
        Hello hello = new Hello();
    }
}

public class Test
{
    public static void main (String [] args)
    {
        Hello hello = new Hello ();
        hello.test ();
    }
}

接下來,你要問你自己,Java 裡 new 出來的物件,到底在是住在記憶體的哪裡?而指到各物件的參考又活在哪裡?是在 Stack 還是在 Heap ?指標裡存的又是什麼?

再來, hello.test() 這一行會翻譯成什麼小人電腦的組合語言?(提示:Method call 和 function call 本質上是一樣的東西)

如果,你能回答出以上的問題,基本上就不會有『為什麼函數沒有定義完還可以呼叫自己,為什麼物件可以 new 自己,為什麼明明就還沒定義完,Node 裡卻可以有 Node』 的這種疑問了。

謎之音:所以說,這篇『爪哇學校的危害』還是有他的道理在的啊!

將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-12-05 (週六) 11:27:06

[Android] 實現 Functional 的 Cursor。

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

退伍了,終於有比較多的時間可以東摸西搞了,想當然爾,現在的我想玩的,就是 Scala + Android 啦!

Android 確實是個很不錯的平台,可是也有他弱的地方,其中我最不喜歡的,就數 Content Provider 啦。

在 Content Provider 的架構下,的確可以達到不同應用程式間溝通的的做用,可是操作的階層實在是太低了,竟然直接動到資料庫的指標去了。

這對於已經漸漸習慣了 Functional 的我而言,真的是很痛苦啊,想到要在那邊 move 來 move 去,就一整個煩人。

幸好,這個時候,很強大的 trait 就登場啦,只要下面短短幾行的程式,馬上就可以把 Cursor 變成 Functional 啦!

class CursorSeq (cursor: Cursor) extends Seq[Cursor]
{
    require (cursor.moveToFirst)
 
    override val length = cursor.getCount
    override val elements = new Iterator[Cursor] {
        override def hasNext = !cursor.isLast &&
                               !cursor.isAfterLast &&
                               !cursor.isClosed
 
        override def next : Cursor = {
            cursor.moveToNext()
            cursor
        }
    }
 
    override def apply (n: Int) : Cursor = {
        cursor.moveToPosition (n)
        cursor
    }
}

然後,再配上一些 Wrapper class……

case class ReminderItem private (id: Int, title: String,
                                 description: String, triggerTime: Int,
                                 maidID : String)
{
    def uri : Uri = Uri.parse (Reminder.ContentUri + "/" + id)
}

object ReminderItem
{
    private def get (cursor: Cursor) : Option[ReminderItem] =
    {
        if (cursor == null) {
            return None
        }
 
        val colID = cursor.getColumnIndex (Reminder.ID)
        val colTitle = cursor.getColumnIndex (Reminder.Title)
        val colDescription = cursor.getColumnIndex (Reminder.Description)
        val colTriggerTime = cursor.getColumnIndex (Reminder.TriggerTime)
        val colMaidID = cursor.getColumnIndex (Reminder.MaidID)
 
        val id = cursor.getInt (colID)
        val title = cursor.getString (colTitle)
        val description = cursor.getString (colDescription)
        val triggerTime = cursor.getInt (colTriggerTime)
        val maidID = cursor.getString (colMaidID)
 
        Some (ReminderItem (id, title, description, triggerTime, maidID))
    }
 
    def getAll (context: Context) : Seq[Option[ReminderItem]] =
    {
        var result : Seq[Option[ReminderItem]] = Nil
        val cursor = new CursorSeq (context.getContentResolver.
                                     query (Reminder.ContentUri,
                                            null, null, null, null))
 
 
        for (row <- cursor) yield get (row)
    }
}

就可以寫出 Functional,並且超簡潔的程式啦!例如:

val item = ReminderItem.getAll.filter (_ != None)

// 取出標題是 Hello 的 ReminderItem
item.filter (i => i.title == "Hello")

// 取出所有 ReminderItem 的 Title
item.map ( i => i.title)

這樣比起直接下 Query 來得清楚多囉。

將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-11-26 (週四) 10:26:29

趁手的兵刃--Scala。

此文評價一顆星二顆星三顆星四顆星五顆星
Loading ... Loading ...

有一些人應該知道,我挺喜歡有事沒事玩一玩一些莫名其妙,沒啥知名度的程式語言,像是之前有玩過 Pike 或是 D  語言之類的。

其實除了好玩外,有一部份也是總覺得自己在正規教育中學的諸如 C  語言或是 Java 這類著名的程式語言,用起來有一些不順手的地方。

畢竟,工欲善其事,必先利其器。就像行走江湖的武林人士都要有一把趁手的兵器一樣,程式與言就如同是程式設計師的兵器,當然希望能夠找到合自己意的,這也是為什麼我之前會看上 D  語言的原因。

但話說回來,有的時候事情沒有那麼美好的啊!選擇這種冷門的程式語言,常常得面對的就是各式各樣支援以及各種函式庫缺乏的問題,D 語言當然也不例外。

而且 D  語言還有許多更嚴重的問題--兩個程式語言規格,兩種標準函式庫,還不夠成熟的編譯器,本來是想簡化 C  以及 C++  的,但最近卻反而愈變愈複雜。

於是最後,我還是選擇放棄了這個看起來不錯,但實際上問題相當多的選擇。

但我還是一直在找趁手的兵刃,最後終於找到啦!那就是我正在用來寫 Android  程式的 Scala  這個程式語言。

雖然和 D  語言比起來,少了系統程式設計方面的優勢(例如指標等底層的操作),但後來我也發現,其實我也根本沒在做系統程式嘛!

以目前我想往 Android  應用程式走的方向而言,Scala 真的是剛剛好啊!

而且與 D  想要在程式碼的層次上向下相容不同,Scala 一開始走的就是程式碼不相容,而是在 Byte Code  上相容的路,於是可以丟掉很多包袱。

總而言之,Scala 提供的諸如 Tuple、Traits、型別推測以及 Closure 以及 Pattern Match 等等,再和 Android 搭起來,讓寫 Android  程式變得超簡單又有趣的啦,不再像以前一樣一堆多餘的語法。

總而言之,接下來就是繼續試驗看看 Scala  是不是一把真的能夠用來行走江湖的好兵器囉!

將本文加入 Hemidemi 書籤
Brian Hsu (墳墓)
2009-09-26 (週六) 16:34:57