Monoid是种最简单的typeclass类型。我们先看看scalaz的Monoid typeclass定义:scalaz/Monoid.scala

 trait Monoid[F] extends Semigroup[F] { self =>
////
/** The identity element for `append`. */
def zero: F
...

Monoid trait又继承了Semigroup:scalaz/Semigroup.scala

 trait Semigroup[F]  { self =>
////
/**
* The binary operation to combine `f1` and `f2`.
*
* Implementations should not evaluate the by-name parameter `f2` if result
* can be determined by `f1`.
*/
def append(f1: F, f2: => F): F
...

所以获取一个类型的Monoid实例需要实现zero和append这两个抽象函数。实际上Monoid typeclass也就是支持了append(|+|)这么一个简单的操作。scalaz为一些标准类型定义了Monoid实例:

  |+|                                          //> res0: Int = 50
.some |+| .some //> res1: Option[Int] = Some(50)
List(,,) |+| List(,,) //> res2: List[Int] = List(1, 2, 3, 4, 5, 6)
Tags.Multiplication() |+| Monoid[Int @@ Tags.Multiplication].zero
//> res3: scalaz.@@[Int,scalaz.Tags.Multiplication] = 3
Tags.Conjunction(true) |+| Tags.Conjunction(false)//> res4: scalaz.@@[Boolean,scalaz.Tags.Conjunction] = false
Tags.Disjunction(true) |+| Tags.Disjunction(false)//> res5: scalaz.@@[Boolean,scalaz.Tags.Disjunction] = true
Monoid[Boolean @@ Tags.Conjunction].zero //> res6: scalaz.@@[Boolean,scalaz.Tags.Conjunction] = true
Monoid[Boolean @@ Tags.Disjunction].zero //> res7: scalaz.@@[Boolean,scalaz.Tags.Disjunction] = false

就这么来看好像没什么值得提的。不过Ordering的Monoid倒是值得研究一下。我们先看看Ordering trait:scalaz/Ordering.scala

  implicit val orderingInstance: Enum[Ordering] with Show[Ordering] with Monoid[Ordering] = new Enum[Ordering] with Show[Ordering] with Monoid[Ordering] {
def order(a1: Ordering, a2: Ordering): Ordering = (a1, a2) match {
case (LT, LT) => EQ
case (LT, EQ | GT) => LT
case (EQ, LT) => GT
case (EQ, EQ) => EQ
case (EQ, GT) => LT
case (GT, LT | EQ) => GT
case (GT, GT) => EQ
} override def shows(f: Ordering) = f.name def append(f1: Ordering, f2: => Ordering): Ordering = f1 match {
case Ordering.EQ => f2
case o => o
}
...

这里定义了Ordering的Monoid实例。它的append函数意思是:两个Ordering类型值f1,f2的append操作结果:假如f1是EQ就是f2,否则是f1:

 (Ordering.EQ: Ordering) |+| (Ordering.GT: Ordering)
//> res8: scalaz.Ordering = GT
(Ordering.EQ: Ordering) |+| (Ordering.LT: Ordering)
//> res9: scalaz.Ordering = LT
(Ordering.GT: Ordering) |+| (Ordering.EQ: Ordering)
//> res10: scalaz.Ordering = GT
(Ordering.LT: Ordering) |+| (Ordering.EQ: Ordering)
//> res11: scalaz.Ordering = LT
(Ordering.LT: Ordering) |+| (Ordering.GT: Ordering)
//> res12: scalaz.Ordering = LT
(Ordering.GT: Ordering) |+| (Ordering.LT: Ordering)
//> res13: scalaz.Ordering = GT

如果我用以上的特性来比较两个String的长度:如果长度相等则再比较两个String的字符顺序。这个要求刚好符合了Ordering Monoid实例的append操作:

  ?|?                                            //> res14: scalaz.Ordering = LT
"abc" ?|? "bac" //> res15: scalaz.Ordering = LT
def strlenCompare(lhs: String, rhs: String): Ordering =
(lhs.length ?|? rhs.length) |+| (lhs ?|? rhs) //> strlenCompare: (lhs: String, rhs: String)scalaz.Ordering strlenCompare("abc","aabc") //> res16: scalaz.Ordering = LT
strlenCompare("abd","abc") //> res17: scalaz.Ordering = GT

这个示范倒是挺新鲜的。

好了,单看Monoid操作会觉着没什么特别,好像不值得研究。实际上Monoid的主要用途是在配合可折叠数据结构(Foldable)对结构内部元素进行操作时使用的。我们再看看这个Foldable typeclass:scalaz/Foldable.scala

 trait Foldable[F[_]]  { self =>
////
import collection.generic.CanBuildFrom
import collection.immutable.IndexedSeq /** Map each element of the structure to a [[scalaz.Monoid]], and combine the results. */
def foldMap[A,B](fa: F[A])(f: A => B)(implicit F: Monoid[B]): B
/** As `foldMap` but returning `None` if the foldable is empty and `Some` otherwise */
def foldMap1Opt[A,B](fa: F[A])(f: A => B)(implicit F: Semigroup[B]): Option[B] = {
import std.option._
foldMap(fa)(x => some(f(x)))
} /**Right-associative fold of a structure. */
def foldRight[A, B](fa: F[A], z: => B)(f: (A, => B) => B): B
...

Foldable typeclass提供了许多注入方法支持折叠操作: scalaz/syntax/FoldableSyntax.scala

 final class FoldableOps[F[_],A] private[syntax](val self: F[A])(implicit val F: Foldable[F]) extends Ops[F[A]] {
////
import collection.generic.CanBuildFrom
import Leibniz.===
import Liskov.<~< final def foldMap[B: Monoid](f: A => B = (a: A) => a): B = F.foldMap(self)(f)
final def foldMap1Opt[B: Semigroup](f: A => B = (a: A) => a): Option[B] = F.foldMap1Opt(self)(f)
final def foldRight[B](z: => B)(f: (A, => B) => B): B = F.foldRight(self, z)(f)
final def foldMapRight1Opt[B](z: A => B)(f: (A, => B) => B): Option[B] = F.foldMapRight1Opt(self)(z)(f)
final def foldRight1Opt(f: (A, => A) => A): Option[A] = F.foldRight1Opt(self)(f)
final def foldLeft[B](z: B)(f: (B, A) => B): B = F.foldLeft(self, z)(f)
final def foldMapLeft1Opt[B](z: A => B)(f: (B, A) => B): Option[B] = F.foldMapLeft1Opt(self)(z)(f)
final def foldLeft1Opt(f: (A, A) => A): Option[A] = F.foldLeft1Opt(self)(f)
final def foldRightM[G[_], B](z: => B)(f: (A, => B) => G[B])(implicit M: Monad[G]): G[B] = F.foldRightM(self, z)(f)
final def foldLeftM[G[_], B](z: B)(f: (B, A) => G[B])(implicit M: Monad[G]): G[B] = F.foldLeftM(self, z)(f)
final def foldMapM[G[_] : Monad, B : Monoid](f: A => G[B]): G[B] = F.foldMapM(self)(f)
final def fold(implicit A: Monoid[A]): A = F.fold(self)(A)
final def foldr[B](z: => B)(f: A => (=> B) => B): B = F.foldr(self, z)(f)
final def foldr1Opt(f: A => (=> A) => A): Option[A] = F.foldr1Opt(self)(f)
final def foldl[B](z: B)(f: B => A => B): B = F.foldl(self, z)(f)
final def foldl1Opt(f: A => A => A): Option[A] = F.foldl1Opt(self)(f)
final def foldrM[G[_], B](z: => B)(f: A => ( => B) => G[B])(implicit M: Monad[G]): G[B] = F.foldrM(self, z)(f)
final def foldlM[G[_], B](z: B)(f: B => A => G[B])(implicit M: Monad[G]): G[B] = F.foldlM(self, z)(f)
final def length: Int = F.length(self)
final def index(n: Int): Option[A] = F.index(self, n)
final def indexOr(default: => A, n: Int): A = F.indexOr(self, default, n)
final def sumr(implicit A: Monoid[A]): A = F.foldRight(self, A.zero)(A.append)
final def suml(implicit A: Monoid[A]): A = F.foldLeft(self, A.zero)(A.append(_, _))
final def toList: List[A] = F.toList(self)
final def toVector: Vector[A] = F.toVector(self)
final def toSet: Set[A] = F.toSet(self)
final def toStream: Stream[A] = F.toStream(self)
final def toIList: IList[A] = F.toIList(self)
final def toEphemeralStream: EphemeralStream[A] = F.toEphemeralStream(self)
final def to[G[_]](implicit c: CanBuildFrom[Nothing, A, G[A]]) = F.to[A, G](self)
final def all(p: A => Boolean): Boolean = F.all(self)(p)
final def ∀(p: A => Boolean): Boolean = F.all(self)(p)
final def allM[G[_]: Monad](p: A => G[Boolean]): G[Boolean] = F.allM(self)(p)
final def anyM[G[_]: Monad](p: A => G[Boolean]): G[Boolean] = F.anyM(self)(p)
final def any(p: A => Boolean): Boolean = F.any(self)(p)
final def ∃(p: A => Boolean): Boolean = F.any(self)(p)
final def count: Int = F.count(self)
final def maximum(implicit A: Order[A]): Option[A] = F.maximum(self)
final def maximumOf[B: Order](f: A => B): Option[B] = F.maximumOf(self)(f)
final def maximumBy[B: Order](f: A => B): Option[A] = F.maximumBy(self)(f)
final def minimum(implicit A: Order[A]): Option[A] = F.minimum(self)
final def minimumOf[B: Order](f: A => B): Option[B] = F.minimumOf(self)(f)
final def minimumBy[B: Order](f: A => B): Option[A] = F.minimumBy(self)(f)
final def longDigits(implicit d: A <:< Digit): Long = F.longDigits(self)
final def empty: Boolean = F.empty(self)
final def element(a: A)(implicit A: Equal[A]): Boolean = F.element(self, a)
final def splitWith(p: A => Boolean): List[NonEmptyList[A]] = F.splitWith(self)(p)
final def selectSplit(p: A => Boolean): List[NonEmptyList[A]] = F.selectSplit(self)(p)
final def collapse[X[_]](implicit A: ApplicativePlus[X]): X[A] = F.collapse(self)
final def concatenate(implicit A: Monoid[A]): A = F.fold(self)
final def intercalate(a: A)(implicit A: Monoid[A]): A = F.intercalate(self, a)
final def traverse_[M[_]:Applicative](f: A => M[Unit]): M[Unit] = F.traverse_(self)(f)
final def traverseU_[GB](f: A => GB)(implicit G: Unapply[Applicative, GB]): G.M[Unit] =
F.traverseU_[A, GB](self)(f)(G)
final def traverseS_[S, B](f: A => State[S, B]): State[S, Unit] = F.traverseS_(self)(f)
final def sequence_[G[_], B](implicit ev: A === G[B], G: Applicative[G]): G[Unit] = F.sequence_(ev.subst[F](self))(G)
final def sequenceS_[S, B](implicit ev: A === State[S,B]): State[S,Unit] = F.sequenceS_(ev.subst[F](self))
def sequenceF_[M[_],B](implicit ev: F[A] <~< F[Free[M,B]]): Free[M, Unit] = F.sequenceF_(ev(self))
final def msuml[G[_], B](implicit ev: A === G[B], G: PlusEmpty[G]): G[B] = F.foldLeft(ev.subst[F](self), G.empty[B])(G.plus[B](_, _))
////
}

这简直就是一个完整的函数库嘛。scalaz为大多数标准库中的集合类型提供了Foldable实例,也就是说大多数scala集合类型都支持这么一堆折叠操作函数。我还看不到任何需要去自定义集合类型,标准库的集合类型加上Foldable typeclass应该足够用了。

在Foldable typeclass中比较重要的函数就是foldMap了:

 trait Foldable[F[_]]  { self =>
////
import collection.generic.CanBuildFrom
import collection.immutable.IndexedSeq /** Map each element of the structure to a [[scalaz.Monoid]], and combine the results. */
def foldMap[A,B](fa: F[A])(f: A => B)(implicit F: Monoid[B]): B

首先,foldMap需要Monoid[B]实例来实现。用List来举例:List trait 继承了Traverse:scalaz/std/List.scala

 trait ListInstances extends ListInstances0 {
implicit val listInstance = new Traverse[List] with MonadPlus[List] with Zip[List] with Unzip[List] with Align[List] with IsEmpty[List] with Cobind[List] {
...

在Traverse typeclass里定义了Foldable实例:scalaz/Traverse.scala

  def foldLShape[A,B](fa: F[A], z: B)(f: (B,A) => B): (B, F[Unit]) =
runTraverseS(fa, z)(a => State.modify(f(_, a))) override def foldLeft[A,B](fa: F[A], z: B)(f: (B,A) => B): B = foldLShape(fa, z)(f)._1 def foldMap[A,B](fa: F[A])(f: A => B)(implicit F: Monoid[B]): B = foldLShape(fa, F.zero)((b, a) => F.append(b, f(a)))._1 override def foldRight[A, B](fa: F[A], z: => B)(f: (A, => B) => B) =
foldMap(fa)((a: A) => (Endo.endo(f(a, _: B)))) apply z
...

这个foldMap就是一个游览可折叠结构的函数。在游览过程中用Monoid append对结构中元素进行操作。值得注意的是这个f: A => B参数:这个函数是用来在append操作之前先对内部元素进行一次转变(transform):

 List(,,) foldMap {x => x}                      //> res18: Int = 6
List(,,) foldMap {x => (x + ).toString} //> res19: String = 456 变成String操作

我们试着用一些实际的例子来示范Monoid的用法。上面提到Monoid在可折叠数据结构里的元素连续处理有着很好的应用,我们先试一个例子:确定一个可折叠数据结构F[A]中的元素A是否排序的:

def ordered(xs: List[Int]): Boolean  //判断xs是否按序排列

由于我们必须游览List xs,所以用Monoid对元素Int进行判断操作是可行的方法。我们先设计一个对比数据结构:

Option[(min: Int, max: Int. ordered: Boolean)], 它记录了当前元素的状态,包括最小,最大,是否排序的:

 /判断xs是否是排序的
def ordered(xs: List[Int]): Boolean = {
val monoid = new Monoid[Option[(Int,Int,Boolean)]] { //对类型Option[(Int,Int,Boolean)]定义一个Monoid实例
def zero = None
def append(a1: Option[(Int,Int,Boolean)], a2: => Option[(Int,Int,Boolean)]) = //对连续两个元素进行对比操作
(a1,a2) match {
case (x,None) => x
case (None,x) => x //保留不为None的状态
case (Some((min1,max1,ord1)),Some((min2,max2,ord2))) => //如果max1 <= min2状态即为true
Some((min1 min min2, max1 max max2, ord1 && ord2 && (max1 <= min2))) //更新min,max和ord
}
} //我们需要把元素转换成Option((Int,Int,Boolean))
(xs.foldMap(i => Option((i, i, true)))(monoid)).map(_._3) getOrElse(true)
} //> ordered: (xs: List[Int])Boolean ordered(List(,,,)) //> res21: Boolean = true
ordered(List(,,,)) //> res22: Boolean = false

注意这个i => Option((i,i,true)) 转换(transform)。

由于Monoid是种极简单的类型,所以很容易对Monoid进行组合。Monoid组合产生的结果还是Monoid,并且用起来可以更方便:

 def productMonoid[A,B](ma: Monoid[A], mb: Monoid[B]): Monoid[(A,B)] = new Monoid[(A,B)] {
def zero = (ma.zero, mb.zero)
def append(x: (A,B), y: => (A,B)): (A,B) = (ma.append(x._1, y._1), mb.append(x._2, y._2))
} //> productMonoid: [A, B](ma: scalaz.Monoid[A], mb: scalaz.Monoid[B])scalaz.Mon
//| oid[(A, B)]
val pm = productMonoid(Monoid[Int],Monoid[List[Int]])
//> pm : scalaz.Monoid[(Int, List[Int])] = Exercises.monoid$$anonfun$main$1$$a
//| non$3@72d1ad2e

以上的pm就是两个Monoid的组合,结果是一个tuple2Monoid。我们可以使用这个tuple2Monoid对可折叠数据结构中元素进行并行操作。比如我们可以在游览一个List[Int]时同时统计长度(list length)及乘积(product):

 val intMultMonoid = new Monoid[Int] {
def zero =
def append(a1: Int, a2: => Int): Int = a1 * a2
} //> intMultMonoid : scalaz.Monoid[Int] = Exercises.monoid$$anonfun$main$1$$ano
//| n$1@6c64cb25
def productMonoid[A,B](ma: Monoid[A], mb: Monoid[B]): Monoid[(A,B)] = new Monoid[(A,B)] {
def zero = (ma.zero, mb.zero)
def append(x: (A,B), y: => (A,B)): (A,B) = (ma.append(x._1, y._1), mb.append(x._2, y._2))
} //> productMonoid: [A, B](ma: scalaz.Monoid[A], mb: scalaz.Monoid[B])scalaz.Mon
//| oid[(A, B)]
val pm = productMonoid(Monoid[Int @@ Tags.Multiplication],Monoid[Int])
//> pm : scalaz.Monoid[(scalaz.@@[Int,scalaz.Tags.Multiplication], Int)] = Exe
//| rcises.monoid$$anonfun$main$1$$anon$3@72d1ad2e
List(,,,,).foldMap(i => (i, ))(productMonoid(intMultMonoid,Monoid[Int]))
//> res23: (Int, Int) = (144,5)

我们再来一个合并多层map的Monoid:

 def mapMergeMonoid[K,V](V: Monoid[V]): Monoid[Map[K, V]] =
new Monoid[Map[K, V]] {
def zero = Map[K,V]()
def append(a: Map[K, V], b: => Map[K, V]) =
(a.keySet ++ b.keySet).foldLeft(zero) { (acc,k) =>
acc.updated(k, V.append(a.getOrElse(k, V.zero),
b.getOrElse(k, V.zero)))
}
} //> mapMergeMonoid: [K, V](V: scalaz.Monoid[V])scalaz.Monoid[Map[K,V]] val M: Monoid[Map[String, Map[String, Int]]] = mapMergeMonoid(mapMergeMonoid(Monoid[Int]))
//> M : scalaz.Monoid[Map[String,Map[String,Int]]] = Exercises.monoid$$anonfun
//| $main$1$$anon$4@79e2c065
val m1 = Map("o1" -> Map("i1" -> , "i2" -> )) //> m1 : scala.collection.immutable.Map[String,scala.collection.immutable.Map[
//| String,Int]] = Map(o1 -> Map(i1 -> 1, i2 -> 2))
val m2 = Map("o1" -> Map("i2" -> )) //> m2 : scala.collection.immutable.Map[String,scala.collection.immutable.Map[
//| String,Int]] = Map(o1 -> Map(i2 -> 3))
val m3 = M.append(m1, m2) //> m3 : Map[String,Map[String,Int]] = Map(o1 -> Map(i1 -> 1, i2 -> 5))

我们可以用这个组合成的M的append操作进行map的深度合并。m1,m2合并后:Map(o1->Map("i1"->1,"i2" -> 5))。

我们还可以用这个Monoid来统计一段字串内字符发生的频率:

 def frequencyMap[A](as: List[A]): Map[A, Int] =
as.foldMap((a: A) => Map(a -> ))(mapMergeMonoid[A, Int](Monoid[Int]))
//> frequencyMap: [A](as: List[A])Map[A,Int]
frequencyMap("the brown quik fox is running quikly".toList)
//> res24: Map[Char,Int] = Map(e -> 1, s -> 1, x -> 1, n -> 4, y -> 1, t -> 1,
//| u -> 3, f -> 1, i -> 4, -> 6, q -> 2, b -> 1, g -> 1, l -> 1, h -> 1, r -
//| > 2, w -> 1, k -> 2, o -> 2)

我们现在可以体会到Monoid必须在可折叠数据结构(Foldable)内才能正真发挥作用。

Scalaz(8)- typeclass:Monoid and Foldable的更多相关文章

  1. Scalaz(4)- typeclass:标准类型-Equal,Order,Show,Enum

    Scalaz是由一堆的typeclass组成.每一个typeclass具备自己特殊的功能.用户可以通过随意多态(ad-hoc polymorphism)把这些功能施用在自己定义的类型上.scala这个 ...

  2. Scalaz(9)- typeclass:checking instance abiding the laws

    在前几篇关于Functor和Applilcative typeclass的讨论中我们自定义了一个类型Configure,Configure类型的定义是这样的: case class Configure ...

  3. Scalaz(7)- typeclass:Applicative-idomatic function application

    Applicative,正如它的名称所示,就是FP模式的函数施用(function application).我们在前面的讨论中不断提到FP模式的操作一般都在管道里进行的,因为FP的变量表达形式是这样 ...

  4. Scalaz(6)- typeclass:Functor-just map

    Functor是范畴学(Category theory)里的概念.不过无须担心,我们在scala FP编程里并不需要先掌握范畴学知识的.在scalaz里,Functor就是一个普通的typeclass ...

  5. Scalaz(5)- typeclass:my typeclass scalaz style-demo

    我们在上一篇讨论中介绍了一些基本的由scalaz提供的typeclass.这些基本typeclass主要的作用是通过操作符来保证类型安全,也就是在前期编译时就由compiler来发现错误.在这篇讨论中 ...

  6. Scalaz(25)- Monad: Monad Transformer-叠加Monad效果

    中间插播了几篇scalaz数据类型,现在又要回到Monad专题.因为FP的特征就是Monad式编程(Monadic programming),所以必须充分理解认识Monad.熟练掌握Monad运用.曾 ...

  7. Scalaz(12)- Monad:再述述flatMap,顺便了解MonadPlus

    在前面的几篇讨论里我们初步对FP有了些少了解:FP嘛,不就是F[A]吗?也是,FP就是在F[]壳子(context)内对程序的状态进行更改,也就是在F壳子(context)内施用一些函数.再直白一点就 ...

  8. Scalaz(43)- 总结 :FP就是实用的编程模式

    完成了对Free Monad这部分内容的学习了解后,心头豁然开朗,存在心里对FP的疑虑也一扫而光.之前也抱着跟大多数人一样的主观概念,认为FP只适合学术性探讨.缺乏实际应用.运行效率低,很难发展成现实 ...

  9. Scalaz(41)- Free :IO Monad-Free特定版本的FP语法

    我们不断地重申FP强调代码无副作用,这样才能实现编程纯代码.像通过键盘显示器进行交流.读写文件.数据库等这些IO操作都会产生副作用.那么我们是不是为了实现纯代码而放弃IO操作呢?没有IO的程序就是一段 ...

随机推荐

  1. Atitit 图像清晰度 模糊度 检测 识别 评价算法 源码实现attilax总结

    Atitit 图像清晰度 模糊度 检测 识别 评价算法 源码实现attilax总结 1.1. 原理,主要使用像素模糊后的差别会变小1 1.2. 具体流程1 1.3. 提升性能 可以使用采样法即可..1 ...

  2. KnockoutJS 3.X API 第三章 计算监控属性(2) 可赋值的计算监控属性

    .liveexample{ padding: 1em; background-color: #eeeedd; border: 1px solid #ccc; max-width: 655px; fon ...

  3. MySQL(三) 数据库表的查询操作【重要】

    序言 1.MySQL表操作(创建表,查询表结构,更改表字段等), 2.MySQL的数据类型(CHAR.VARCHAR.BLOB,等), 本节比较重要,对数据表数据进行查询操作,其中可能大家不熟悉的就对 ...

  4. 分享系列--面试JAVA架构师--链家网

    本月7日去了一趟链家网面试,虽然没有面上,但仍有不少收获,在此做个简单的分享,当然了主要是分享给自己,让大家见笑了.因为这次是第一次面试JAVA网站架构师相关的职位,还是有些心虚的,毕竟之前大部分时间 ...

  5. Win10下PB停在欢迎窗口界面

    问题:Win10下不能打开PB12.5,PB12.6,一直停在欢迎窗口界面. 解决方法:把服务"Touch Keyboard and Handwriting Panel Service&qu ...

  6. SSRS2:Reporting Service 配置Service Account

    1,Service Account SSRS以一个Service方式实现,有三部分组成:Web Service,Report Manager和一个后台的进程,这个Service运行的账号就是Servi ...

  7. 浅谈Winform事件的实现以及模拟其事件的实现(附实现源码)

    当我们初学Winform的时候被其神奇的事件功能所吸引,当点击一个按钮时,便会跳到我们所写的点击方法当中去.然而这并不符合我们对方法的理解,究竟.net在后面帮助我们实现了什么.我们怎样模拟其事件的实 ...

  8. WP中的语音识别(上):基本识别

    WP 8.1目前许多内容仍处于未确定状态,因此,本文所提及的语音识别,是基于WP8的,在8.1中也差不多,也是使用运行时API来实现,如果大家不知道什么是运行时API,也没关系,不影响学习和开发,因为 ...

  9. LoRaWAN协议(六)--OTAA KEY生成过程

    前言 通过OTAA方式入网的设备,通讯时使用的KEY需要通过服务器获得,在入网之间,设备无法通讯. 相关的OTAA入网流程已经在上一章中讲解过了,有兴趣的可以去看看**LoRaWAN协议(五)__OT ...

  10. Mybatis之Oracle增删查改示例--转

    http://blog.csdn.net/bingjie1217/article/details/21088431?utm_source=tuicool&utm_medium=referral ...