Scalaz(25)- Monad: Monad Transformer-疊加Monad效果

来源:http://www.cnblogs.com/tiger-xc/archive/2016/01/20/5143993.html
-Advertisement-
Play Games

中間插播了幾篇scalaz數據類型,現在又要回到Monad專題。因為FP的特征就是Monad式編程(Monadic programming),所以必須充分理解認識Monad、熟練掌握Monad運用。曾經看到一段對Monad的描述:“Monadic for-comprehension就是一種嵌入式編....


  中間插播了幾篇scalaz數據類型,現在又要回到Monad專題。因為FP的特征就是Monad式編程(Monadic programming),所以必須充分理解認識Monad、熟練掌握Monad運用。曾經看到一段對Monad的描述:“Monadic for-comprehension就是一種嵌入式編程語言,由它的Monad提供它的語法”。但如果每一種Monad的for-comprehension都獨立提供一套語法的話,這種編程語言就顯得十分單調、功能簡單了。那麼既然是FP,我們應該可以通過函數組合(function composition)來把很多簡單的for-comprehension組合成一套完善的編程語言吧?比如這樣:Option[A] >>> IO[Option[A]] >>> IO[Either[String,Option[A]]。恰恰,Monad是不支持函數組合的。先瞭解一下函數組合:Functor是可以組合的,我們可以把fa和fb組合成一個更複雜的Functor fab,我們來驗證一下:

 def composeFunctor[M[_],N[_]](fa: Functor[M], fb: Functor[N]): Functor[({type mn[x] = M[N[x]]})#mn] =
   new Functor[({type mn[x] = M[N[x]]})#mn] {
        def map[A,B](fab: M[N[A]])(f: A => B): M[N[B]] =
           fa.map(fab)(n => fb.map(n)(f))
   }                                              //> composeFunctor: [M[_], N[_]](fa: scalaz.Functor[M], fb: scalaz.Functor[N])s
                                                  //| calaz.Functor[[x]M[N[x]]]

我們來解釋一下:如果M,N都是Functor,那麼M[N[A]]也是Functor,我們可以用M[N[A]].map來運算A值。看看下麵的例子:

1 val stringlen: String => Int = _.length           //> stringlen  : String => Int = <function1>
2 val optionInList = List("1".some,"12".some,"123".some)
3                                                   //> optionInList  : List[Option[String]] = List(Some(1), Some(12), Some(123))
4 
5 val mnFunctor = composeFunctor(Functor[List],Functor[Option])
6                                                   //> mnFunctor  : scalaz.Functor[[x]List[Option[x]]] = Exercises.monadtrans$$ano
7                                                   //| nfun$main$1$$anon$1@130d63be
8 mnFunctor.map(optionInList)(stringlen)            //> res3: List[Option[Int]] = List(Some(1), Some(2), Some(3))

那麼我們需要的Monad組合應該是這樣的:M[N[A]],M,N都是Monad,如:Either[String,Option[A]],甚至是M[N[P[A]]],三層Monad。可惜,不是所有Monad都支持函數組合的,看下麵:

 def composeMonad[M[_],N[_]](ma: Monad[M], mb: Monad[N]): Monad[({type mn[x] = M[N[x]]})#mn] =
   new Monad[({type mn[x] = M[N[x]]})#mn] {
     def point[A](a: => A) = ma.point(mb.point(a))
        def bind[A,B](mab: M[N[A]])(f: A => M[N[B]]): M[N[B]] =
           ??? ...
   }

實現M[N[A]].bind是不可能的,大家可以試試。這就堵死了函數組合這條路。難道我們就無法使用M[N[A]]這樣的for-comprehension了嗎?畢竟像Either[String,Option[A]]這樣的組合是很普遍的啊,比如說從資料庫里讀取這樣的動作,有幾種可能:取得數據、無數據None、發生錯誤。無論如何我們先試試用for-comprehension:

1 type Result[A] = String \/ Option[A]
2 val result: Result[Int] = 62.some.right           //> result  : Exercises.monadtxns.Result[Int] = \/-(Some(62))
3 for {
4     optionValue <- result
5 } yield {
6   for {
7       valueA <- optionValue
8   } yield valueA + 18                             //> res0: scalaz.\/[String,Option[Int]] = \/-(Some(80))
9 }

從上面可以瞭解我們必須用兩層for-comprehension才能運算A值。那麼可想而知如果是M[N[P[A]]]就需要三層for-comprehension了。這就是所謂的“下階梯式演算法”(stair-stepping)。錶面上來看stair-stepping會產生複雜臃腫的代碼,喪失FP的精簡優雅風格。但想深一層,如果其中一個Monad是會產生副作用的如IO[Option[A]],那麼上面的例子就變成這樣:

1 for {
2   optionData <- IO
3 } yield {
4   for {
5     data <- optionData
6   } yield Process(data)
7 }

我們看到在第一層運算里進行了IO運算,產生了副作用。那麼以上的代碼就不再是純代碼了,無法保障函數組合。也就是說stair-stepping會產生不純代碼,違背了FP要求。之前我們曾經討論過 ReaderWriterState Monad,它是Reader,Writer,State三個Monad的組合。在它的for-comprehension里的運算結果類型是ReaderWriterState一種,所以沒有stair-stepping憂慮。但我們必須先創建一個新的類型(不是通過函數組合的新類型)。難道我們在使用不同要求的for-comprehension時都需要重新創建一個新類型嗎,這樣不就損失了FP的代碼重覆使用特點了嗎?不,scalaz提供的Monad Transformer就是一個有效的解決方案。

scalaz為很多type class提供了Monad Transformer,它們都以T尾綴命名如OptionT、EitherT、StateT...,我們可以通過Monad Transformer來靈活地組合Monad。以OptionT為例:

1 type Error[A] = \/[String, A]
2 type Result[A] = OptionT[Error, A]
3 
4 val result: Result[Int] = 62.point[Result]        //> result  : Exercises.monadtxns.Result[Int] = OptionT(\/-(Some(62)))
5 val transformed =
6   for {
7     value <- result
8   } yield value + 18                              //> transformed  : scalaz.OptionT[Exercises.monadtxns.Error,Int] = OptionT(\/-(S
9                                                   //| ome(80)))

現在,運算A只需要一層context了。Result就是通過Monad Transformer產生的新類型。在上面的類型構建里,OptionT就是一個Monad Transformer、Error是固定了Left類型的Either。因為Either有兩個類型參數,我們實際上也可以直接用type lambda來表示Result[A]:

type Result[A] = OptionT[({type l[x] = \/[String,x]})#l,A]

不過這樣寫不但複雜,而且會影響編譯器的類型推導(compiler type inference)。

值得註意的是,Monad Transformer 類型的構建是由內向外反向的。比如上面的例子中OptionT是個Monad Transformer,它的類型款式是OptionT[M[_],A]。OptionT實際上是用來構建M[Option[A]],在我們的例子里就是Either[Option[A]]。我們來看看一些常用Monad Transformer的類型款式:

final case class OptionT[F[_], A](run: F[Option[A]]) {
...
final case class EitherT[F[_], A, B](run: F[A \/ B]) {
...
final case class ListT[F[_], A](run: F[List[A]]){
...
trait IndexedStateT[F[_], -S1, S2, A] { self =>
  /** Run and return the final value and state in the context of `F` */
  def apply(initial: S1): F[(S2, A)]

可以看到,Monad Transformer 的主要作用就在構成run這個我們稱為嵌入值了。F可以是任何普通Monad。在上面的例子就變成了:

OptionT[Either,A](run: Either[Option[A]]),這個Either[Option[A]]就是我們的目標類型。而我們在操作時如在for-comprehension中運算時使用的類型則必須統一為OptionT[Either,A]。

我們如何去構建Monad Transformer類型值呢?我們可以用Applicative[MT].point或者直接用構建器方式如OptionT(...)

//point升格
Applicative[Result].point(62)                     //> res0: Exercises.monadtxns.Result[Int] = OptionT(\/-(Some(62)))
//簡寫版本
62.point[Result]                                  //> res1: Exercises.monadtxns.Result[Int] = OptionT(\/-(Some(62)))
//會產生錯誤結果
None.point[Result]                                //> res2: Exercises.monadtxns.Result[None.type] = OptionT(\/-(Some(None)))
"Oh,shit!".left.point[Result]                     //> res3: Exercises.monadtxns.Result[scalaz.\/[String,Nothing]] = OptionT(\/-(So
                                                  //| me(-\/(Oh,shit!))))
//用構建器
OptionT((None: Option[Int]).point[Error])         //> res4: scalaz.OptionT[Exercises.monadtxns.Error,Int] = OptionT(\/-(None))
OptionT(none[Int].point[Error])                   //> res5: scalaz.OptionT[Exercises.monadtxns.Error,Int] = OptionT(\/-(None))
OptionT("Oh,shit!".left: Error[Option[Int]])      //> res6: scalaz.OptionT[Exercises.monadtxns.Error,Int] = OptionT(-\/(Oh,shit!))

與重新構建另一個類型不同的是,通過Monad Transformer疊加Monad組合形成類型的操作依然使用各組成Monad的操作函數,這些函數運算結果類型任然是對應的Monad類型,所以需要一些升格函數(lifting functions)來統一類型。而重建類型則繼承了組成Monad的操作函數,它們的運算結果類型都與新建的這個類型一致。下麵我們還是用上面的這個Either+Option例子來示範。我們把Either和Option疊加後按照不同順序可以產生Either[Option[A]]或者Option[Either[A]]兩種結果類型,所以疊加順序是非常重要的,因為這兩種類型代表著截然不同的意義:Either[Option[A]]代表一個運算結果可以是成功right或者失敗left,如果運算成功則返回一個結果或空值;而Option[Either[A]]從字面上理解好像是一個運算可以返回一個成功或失敗的運算又或者返回空值,應該是沒有任何意義的一個類型。前面我們提到過用Monad Transformer疊加Monad是由內向外反方向的:獲取Either[Option[A]]就需要用OptionT[Either,A]。而且我們需要把Either和Option升格成OptionT[Either,A],看下麵的示範:

 1 type Error[A] = \/[String, A]
 2 type Result[A] = OptionT[Error, A]
 3 
 4 def getString: Option[String] = "Hello ".some     //> getString: => Option[String]
 5 def getResult: Error[String] = "how are you!".right
 6                                                   //> getResult: => Exercises.monadtxns.Error[String]
 7 val prg: Result[String] = for {
 8   s1 <- OptionT.optionT(getString.point[Error])
 9   s2 <- "World,".point[Result]
10   s3 <- getResult.liftM[OptionT]
11 } yield s1+s2+s3                                  //> prg  : Exercises.monadtxns.Result[String] = OptionT(\/-(Some(Hello World,how
12                                                   //|  are you!)))
13 prg.run                                           //> res0: Exercises.monadtxns.Error[Option[String]] = \/-(Some(Hello World,how a
14                                                   //| re you!))

首先,我們避免了stair-stepping,直接運算s1+s2+s3。point、OptionT.optionT、liftM分別對String,Option,Either進行類型升格形成Result[String] >>> OptionT[Error,String]。升格函數源代碼如下:

trait ApplicativeIdV[A] extends Ops[A] {
    def point(implicit F: Applicative[F]): F[A] = Applicative[F].point(self)
...
trait OptionTFunctions {
  def optionT[M[_]] = new (({type λ[α] = M[Option[α]]})#λ ~> ({type λ[α] = OptionT[M, α]})#λ) {
    def apply[A](a: M[Option[A]]) = new OptionT[M, A](a)
  }
...
final class MonadOps[F[_],A] private[syntax](val self: F[A])(implicit val F: Monad[F]) extends Ops[F[A]] {
  ////

  def liftM[G[_[_], _]](implicit G: MonadTrans[G]): G[F, A] = G.liftM(self)
...

再看看組合的Monad是否實現了功能疊加,如果我們加個None轉換:

1 val prg: Result[String] = for {
2   s1 <- OptionT.optionT(getString.point[Error])
3   s0 <- OptionT(none[String].point[Error])  
4   s2 <- "World,".point[Result]
5   s3 <- getResult.liftM[OptionT]
6 } yield s1+s2+s3                                  //> prg  : Exercises.monadtxns.Result[String] = OptionT(\/-(None))
7 prg.run                                           //> res0: Exercises.monadtxns.Error[Option[String]] = \/-(None)

加個Left效果:

1 val prg: Result[String] = for {
2   s1 <- OptionT.optionT(getString.point[Error])
3   s0 <- OptionT("Catch Error!".left: Error[Option[String]])
4   s2 <- "World,".point[Result]
5   s3 <- getResult.liftM[OptionT]
6 } yield s1+s2+s3                                  //> prg  : Exercises.monadtxns.Result[String] = OptionT(-\/(Catch Error!))
7 prg.run                                           //> res0: Exercises.monadtxns.Error[Option[String]] = -\/(Catch Error!)

的確,用Monad Transformer組合Monad後可以實現成員Monad的效果疊加。

不過,在實際應用中兩層以上的Monad組合還是比較普遍的。Monad Transformer本身就是Monad,可以繼續與另一個Monad組合,只要用這個Monad的Transformer就行了。例如我們在上面的例子里再增加一層State,最終形成一個三層類型:State[Either[Option[A]]]。按照上面的經驗,堆砌Monad是由內向外的,我們先組合 StateEither >>> StateT[Either,A],然後再得出組合:OptionT[StateEither,A]。我們來示範一下:

先重新命名(alias)一些類:

type StringEither[A] = String \/ A
type StringEitherT[M[_],A] = EitherT[M,String,A]
type IntState[A] = State[Int,A]
type IntStateT[M[_],A] = StateT[M,Int,A]
type StateEither[A] = StringEitherT[IntState,A]
type StateEitherOption[A] = OptionT[StateEither,A]

由Option,Either,State組合而成的Monad需要相關的升格函數(lifting functions):

//常量升格
val m: StateEitherOption[Int] = 3.point[StateEitherOption]
                                                  //> m  : Exercises.monad_txnfm.StateEitherOption[Int] = OptionT(EitherT(scalaz.p
                                                  //| ackage$StateT$$anon$1@4f638935))
//option類升格
val o: Option[Int] = 3.some                       //> o  : Option[Int] = Some(3)
val o1: StateEither[Option[Int]]= o.point[StateEither]
                                                  //> o1  : Exercises.monad_txnfm.StateEither[Option[Int]] = EitherT(scalaz.packag
                                                  //| e$StateT$$anon$1@694abbdc)

val o2: StateEitherOption[Int] = OptionT.optionT(o1)
                                                  //> o2  : Exercises.monad_txnfm.StateEitherOption[Int] = OptionT(EitherT(scalaz.
                                                  //| package$StateT$$anon$1@694abbdc))
//val o2: OptionT[StateEither,Int] = OptionT.optionT(o1)

//either類升格
val e: StringEither[Int] = 3.point[StringEither]  //> e  : Exercises.monad_txnfm.StringEither[Int] = \/-(3)
val e1: IntState[StringEither[Int]] = e.point[IntState]
                                                  //> e1  : Exercises.monad_txnfm.IntState[Exercises.monad_txnfm.StringEither[Int]
                                                  //| ] = scalaz.package$StateT$$anon$1@52bf72b5
val e2: StateEither[Int] = EitherT.eitherT(e1)    //> e2  : Exercises.monad_txnfm.StateEither[Int] = EitherT(scalaz.package$StateT
                                                  //| $$anon$1@52bf72b5)
//val e2: StringEitherT[IntState,Int] = EitherT.eitherT(e1)
val e3: StateEitherOption[Int] = e2.liftM[OptionT]//> e3  : Exercises.monad_txnfm.StateEitherOption[Int] = OptionT(EitherT(scalaz.
                                                  //| IndexedStateT$$anon$10@2d7275fc))
//val e3: OptionT[StateEither,Int] = e2.liftM[OptionT]
//state類升格
val s: IntState[Int] = get[Int]                   //> s  : Exercises.monad_txnfm.IntState[Int] = scalaz.package$State$$anon$3@7e0
                                                  //| 7db1f
val s1: StateEither[Int] = s.liftM[StringEitherT] //> s1  : Exercises.monad_txnfm.StateEither[Int] = EitherT(scalaz.IndexedStateT
                                                  //| $$anon$10@8f4ea7c)
//val s1: StringEitherT[IntState,Int] = s.liftM[StringEitherT]
val s2: StateEitherOption[Int] = s1.liftM[OptionT]//> s2  : Exercises.monad_txnfm.StateEitherOption[Int] = OptionT(EitherT(scalaz
                                                  //| .IndexedStateT$$anon$10@436813f3))
//val s2: OptionT[StateEither,Int] = s1.liftM[OptionT]
//把State升格成StateT
val s3: IntStateT[StringEither,Int] = get[Int].lift[StringEither]
                                                  //> s3  : Exercises.monad_txnfm.IntStateT[Exercises.monad_txnfm.StringEither,In
                                                  //| t] = scalaz.IndexedStateT$$anon$7@10e31a9a

上面又多介紹了StateT.lift, EitherT.eitherT兩個升格函數:

  def lift[M[_]: Applicative]: IndexedStateT[({type λ[α]=M[F[α]]})#λ, S1, S2, A] = new IndexedStateT[({type λ[α]=M[F[α]]})#λ, S1, S2, A] {
    def apply(initial: S1): M[F[(S2, A)]] = Applicative[M].point(self(initial))
  }
...
trait EitherTFunctions {
  def eitherT[F[_], A, B](a: F[A \/ B]): EitherT[F, A, B] = EitherT[F, A, B](a)
...

我們在上面例子的基礎上增加一層State效果後再試用一下這些升格函數:

 1 def getString: Option[String] = "Hello ".some     //> getString: => Option[String]
 2 def getResult: StringEither[String] = "how are you!".right[String]
 3                                                   //> getResult: => Exercises.monad_txnfm.StringEither[String]
 4 def modState(s:Int): IntState[Unit] = put(s)      //> modState: (s: Int)Exercises.monad_txnfm.IntState[Unit]
 5 val prg: StateEitherOption[String] = for {
 6   s1 <- OptionT.optionT(getString.point[StateEither])
 7   s2 <- "World,".point[StateEitherOption]
 8   s3 <- (EitherT.eitherT(getResult.point[IntState]): StateEither[String]).liftM[OptionT]
 9   _ <- (modState(99).liftM[StringEitherT]: StateEither[Unit]).liftM[OptionT]
10 } yield s1+s2+s3                                  //> prg  : Exercises.monad_txnfm.StateEitherOption[String] = OptionT(EitherT(sc
11                                                   //| alaz.IndexedStateT$$anon$10@158d2680))
12 prg.run                                           //> res0: Exercises.monad_txnfm.StateEither[Option[String]] = EitherT(scalaz.In
13                                                   //| dexedStateT$$anon$10@158d2680)

不錯,類型對了,prg可以通過編譯,但未免複雜了點。我花了許多時間去匹配這些類型,因為需要連續升格。可想而知,如果遇到四層以上的Monad組合,代碼會複雜成怎樣。其中重點還是在各種類型的升格。那我們還是回顧一下這些升格函數吧:

 

A.point[F[_]] >>> F[A]   "hi".point[Option] = Option[String] = Some("hi")

 

M[A].liftM[T[_[_],_]] >>> T[M,A]   List(3).liftM[OptionT] = OptionT[List,Int] = OptionT(List(Some(3)))

 

OptionT.optionT(M[Option[A]]) >>> OptionT[M,A]  OptionT.optionT(List(3.some)) = OptionT[List,Int] = OptionT(List(Some(3)

 

EitherT.eitherT(M[Either[A]]) >>> EitherT[M,A] EitherT.eitherT(List(3.right[String])) = EitherT(List(\/-(3))

 

State.lift[M[A]] >>> StateT[M,A]  get[Int].lift[Option] = StateT[Option,Int]

 

註意:以上採用了形象類型表述

 

 

 

 

 

 

 

 

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • WCF
    正在準備中
  • 9-5. 刪除一個斷開的實體問題我們要把一個把WCF上取回的對象做上刪除的標誌.解決方案假設我們有如Figure 9-5所示實體的支付與票據的模型.Figure 9-5. 一個支付與票據的模型我們的模型展示了支付記錄與票據的關係。在應用程式中,我們用客戶端與用WC封裝EF數據訪問交互. 在我們的例子...
  • 想完成一個鏈表發現有錯誤,代碼如下://http://ac.jobdu.com/problem.php?pid=1511//֮ǰÓÃlistʵÏֵģ¬½ñÌìÊÔÒ»ÏÂÓÃstructʵÏÖһϰÉ//¿
  • 在try-catch-finally語句中使用return語句遇到了一些疑問代碼一:static int intc(){ int x =0; try{ x=1; return x; }finally { ...
  • 我又來送福利啦!!!不同於上篇文章,這次我們的爬蟲採用了多線程,一直以來被所謂的分散式 多線程 爬蟲 給唬的怕怕的。今天就來一發多線程爬蟲吧,還能看妹子圖,想想就覺得很激動!!! 依然是流程解釋: 1.分析要爬取的網址,發現頁面分兩級,第一級是多個圖片集的入口,第二集是圖片的入口。我們新建兩...
  • 前面幾篇博客已經講到了關於0V7725的相關驅動問題,那麼OV7725驅動成功之後,設定OV7725輸出RGB565格式,那麼對於640x480x16,那麼若是選用FIFO,應該設置為位寬16bit,存儲深度為30萬,但是這樣是不現實的。所以採用存儲深度更大的SDRAM來實現數據的緩存。 要麼對於....
  • Python語言介紹 Python創始人:Guido(龜叔),Python的名字來自電視劇Monty Python's Flying Circus(創造一種C和shell之間,功能全,易學易用,可拓展),1991年第一個Python編輯器誕生。 Python哲學思想:簡單,優雅,明確 Py...
  • 有時候刪除windows中的目錄的時候,會出現"源文件名長度大於系統支持的長度", 而導致不能刪除, 作為一個程式猿, 怎麼可以被這個折服呢, 原理: 利用 Java 遞歸刪除文件.上代碼:import java.io.File; /** * @author 吳慶龍 * 2015年10月13日...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...