简体   繁体   English

Not a Functor/Functor/Applicative/Monad 的好例子?

[英]Good examples of Not a Functor/Functor/Applicative/Monad?

While explaining to someone what a type class X is I struggle to find good examples of data structures which are exactly X.在向某人解释什么是类型类 X 时,我很难找到恰好是 X 的数据结构的好例子。

So, I request examples for:因此,我请求以下示例:

  • A type constructor which is not a Functor.不是 Functor 的类型构造函数。
  • A type constructor which is a Functor, but not Applicative.一个类型构造函数,它是一个 Functor,但不是 Applicative。
  • A type constructor which is an Applicative, but is not a Monad.一个类型构造函数,它是一个 Applicative,但不是一个 Monad。
  • A type constructor which is a Monad.一个类型构造函数,它是一个 Monad。

I think there are plenty examples of Monad everywhere, but a good example of Monad with some relation to previous examples could complete the picture.我认为到处都有很多 Monad 的例子,但是一个很好的 Monad 例子与之前的例子有一些关系可以完成这幅画。

I look for examples which would be similar to each other, differing only in aspects important for belonging to the particular type class.我寻找彼此相似的示例,仅在对属于特定类型类的重要方面有所不同。

If one could manage to sneak up an example of Arrow somewhere in this hierarchy (is it between Applicative and Monad?), that would be great too!如果有人可以设法在这个层次结构中的某个地方偷偷摸摸一个 Arrow 的例子(它是在 Applicative 和 Monad 之间吗?),那也太棒了!

A type constructor which is not a Functor:不是 Functor 的类型构造函数:

newtype T a = T (a -> Int)

You can make a contravariant functor out of it, but not a (covariant) functor.你可以用它制作一个逆变函子,但不能制作一个(协变)函子。 Try writing fmap and you'll fail.尝试编写fmap并且你会失败。 Note that the contravariant functor version is reversed:请注意,逆变函子版本是相反的:

fmap      :: Functor f       => (a -> b) -> f a -> f b
contramap :: Contravariant f => (a -> b) -> f b -> f a

A type constructor which is a functor, but not Applicative:一个类型构造函数,它是一个函子,但不是 Applicative:

I don't have a good example.我没有很好的例子。 There is Const , but ideally I'd like a concrete non-Monoid and I can't think of any.Const ,但理想情况下我想要一个具体的非 Monoid 并且我想不出任何。 All types are basically numeric, enumerations, products, sums, or functions when you get down to it.所有类型基本上都是数字、枚举、乘积、总和或函数。 You can see below pigworker and I disagreeing about whether Data.Void is a Monoid ;你可以在 pigworker 下面看到我不同意Data.Void是否是Monoid

instance Monoid Data.Void where
    mempty = undefined
    mappend _ _ = undefined
    mconcat _ = undefined

Since _|_ is a legal value in Haskell, and in fact the only legal value of Data.Void , this meets the Monoid rules.由于_|_在 Haskell 中是合法值,并且实际上是Data.Void的唯一合法值, Data.Void这符合 Monoid 规则。 I am unsure what unsafeCoerce has to do with it, because your program is no longer guaranteed not to violate Haskell semantics as soon as you use any unsafe function.我不确定unsafeCoerce与它有什么关系,因为一旦您使用任何unsafe函数,您的程序就不再保证不会违反 Haskell 语义。

See the Haskell Wiki for an article on bottom ( link ) or unsafe functions ( link ).有关底部(链接)或不安全函数(链接)的文章,请参阅 Haskell Wiki。

I wonder if it is possible to create such a type constructor using a richer type system, such as Agda or Haskell with various extensions.我想知道是否可以使用更丰富的类型系统来创建这样的类型构造函数,例如具有各种扩展的 Agda 或 Haskell。

A type constructor which is an Applicative, but not a Monad:一个类型构造函数,它是一个 Applicative,但不是一个 Monad:

newtype T a = T {multidimensional array of a}

You can make an Applicative out of it, with something like:你可以用它来制作一个 Applicative,比如:

mkarray [(+10), (+100), id] <*> mkarray [1, 2]
  == mkarray [[11, 101, 1], [12, 102, 2]]

But if you make it a monad, you could get a dimension mismatch.但是如果你让它成为一个 monad,你可能会遇到维度不匹配。 I suspect that examples like this are rare in practice.我怀疑这样的例子在实践中很少见。

A type constructor which is a Monad:一个类型构造函数,它是一个 Monad:

[]

About Arrows:关于箭头:

Asking where an Arrow lies on this hierarchy is like asking what kind of shape "red" is.询问箭头在此层次结构上的位置就像询问“红色”是什么形状。 Note the kind mismatch:注意种类不匹配:

Functor :: * -> *
Applicative :: * -> *
Monad :: * -> *

but,但,

Arrow :: * -> * -> *

My style may be cramped by my phone, but here goes.我的风格可能会被我的手机局促,但在这里。

newtype Not x = Kill {kill :: x -> Void}

cannot be a Functor.不能是函子。 If it were, we'd have如果是这样,我们会有

kill (fmap (const ()) (Kill id)) () :: Void

and the Moon would be made of green cheese.月亮将由绿色奶酪制成。

Meanwhile同时

newtype Dead x = Oops {oops :: Void}

is a functor是函子

instance Functor Dead where
  fmap f (Oops corpse) = Oops corpse

but cannot be applicative, or we'd have但不能适用,否则我们会有

oops (pure ()) :: Void

and Green would be made of Moon cheese (which can actually happen, but only later in the evening).而 Green 将由 Moon cheese 制成(这实际上可能发生,但只能在晚上晚些时候)。

(Extra note: Void , as in Data.Void is an empty datatype. If you try to use undefined to prove it's a Monoid, I'll use unsafeCoerce to prove that it isn't.) (额外注意: Void ,如Data.Void是一个空数据类型。如果你尝试使用undefined来证明它是 Monoid,我将使用unsafeCoerce来证明它不是。)

Joyously,欣喜若狂,

newtype Boo x = Boo {boo :: Bool}

is applicative in many ways, eg, as Dijkstra would have it,在很多方面都适用,例如,正如 Dijkstra 所说,

instance Applicative Boo where
  pure _ = Boo True
  Boo b1 <*> Boo b2 = Boo (b1 == b2)

but it cannot be a Monad.但它不能是一个 Monad。 To see why not, observe that return must be constantly Boo True or Boo False , and hence that要知道为什么不,请观察 return 必须始终为Boo TrueBoo False ,因此

join . return == id

cannot possibly hold.不可能持有。

Oh yeah, I nearly forgot哦对了,差点忘了

newtype Thud x = The {only :: ()}

is a Monad.是一个单子。 Roll your own.自己卷。

Plane to catch...赶飞机...

I believe the other answers missed some simple and common examples:我相信其他答案遗漏了一些简单而常见的例子:

A type constructor which is a Functor but not an Applicative.一个类型构造函数,它是一个 Functor 但不是一个 Applicative。 A simple example is a pair:一个简单的例子是一对:

instance Functor ((,) r) where
    fmap f (x,y) = (x, f y)

But there is no way how to define its Applicative instance without imposing additional restrictions on r .但是没有办法如何定义它的Applicative实例而不对r施加额外的限制。 In particular, there is no way how to define pure :: a -> (r, a) for an arbitrary r .特别是,无法为任意r定义pure :: a -> (r, a)

A type constructor which is an Applicative, but is not a Monad.一个类型构造函数,它是一个 Applicative,但不是一个 Monad。 A well-known example is ZipList .一个众所周知的例子是ZipList (It's a newtype that wraps lists and provides different Applicative instance for them.) (这是一个newtype一个包装清单,并提供不同的Applicative为他们的实例。)

fmap is defined in the usual way. fmap以通常的方式定义。 But pure and <*> are defined aspure<*>被定义为

pure x                    = ZipList (repeat x)
ZipList fs <*> ZipList xs = ZipList (zipWith id fs xs)

so pure creates an infinite list by repeating the given value, and <*> zips a list of functions with a list of values - applies i -th function to i -th element. so pure通过重复给定的值创建一个无限列表,并且<*>使用值列表压缩函数列表 - 将第i个函数应用于第i个元素。 (The standard <*> on [] produces all possible combinations of applying i -th function to j -th element.) But there is no sensible way how to define a monad (see this post ). []上的标准<*>产生将第i个函数应用于第j个元素的所有可能组合。)但是没有明智的方法来定义 monad(请参阅这篇文章)。


How arrows fit into the functor/applicative/monad hierarchy?箭头如何适应 functor/applicative/monad 层次结构? See Idioms are oblivious, arrows are meticulous, monads are promiscuous by Sam Lindley, Philip Wadler, Jeremy Yallop.参见 Sam Lindley、Philip Wadler、Jeremy Yallop 的习语是无知的,箭头是细致的,单子是混杂的。 MSFP 2008. (They call applicative functors idioms .) The abstract: MSFP 2008。(他们称应用函子成语。)摘要:

We revisit the connection between three notions of computation: Moggi's monads, Hughes's arrows and McBride and Paterson's idioms (also called applicative functors).我们重新审视计算的三个概念之间的联系:Moggi 的单子、Hughes 的箭头以及 McBride 和 Paterson 的习语(也称为应用函子)。 We show that idioms are equivalent to arrows that satisfy the type isomorphism A ~> B = 1 ~> (A -> B) and that monads are equivalent to arrows that satisfy the type isomorphism A ~> B = A -> (1 ~> B).我们证明了习语等价于满足类型同构 A ~> B = 1 ~> (A -> B) 的箭头,并且单子等价于满足类型同构 A ~> B = A -> (1 ~ > B)。 Further, idioms embed into arrows and arrows embed into monads.此外,习语嵌入到箭头中,而箭头嵌入到单子中。

一个不是函子的类型构造函数的一个很好的例子是Set :你不能实现fmap :: (a -> b) -> fa -> fb ,因为没有额外的约束Ord b你不能构造fb

I'd like to propose a more systematic approach to answering this question, and also to show examples that do not use any special tricks like the "bottom" values or infinite data types or anything like that.我想提出一种更系统的方法来回答这个问题,并展示一些不使用任何特殊技巧的例子,比如“底部”值或无限数据类型或类似的东西。

When do type constructors fail to have type class instances?什么时候类型构造函数没有类型类实例?

In general, there are two reasons why a type constructor could fail to have an instance of a certain type class:通常,类型构造函数无法拥有某个类型类的实例有两个原因:

  1. Cannot implement the type signatures of the required methods from the type class.无法从类型类实现所需方法的类型签名。
  2. Can implement the type signatures but cannot satisfy the required laws.可以实现类型签名但不能满足所需的法律。

Examples of the first kind are easier than those of the second kind because for the first kind, we just need to check whether one can implement a function with a given type signature, while for the second kind, we are required to prove that no implementation could possibly satisfy the laws.第一种的例子比第二种容易,因为对于第一种,我们只需要检查一个给定类型签名的函数是否可以实现,而对于第二种,我们需要证明没有实现可能满足法律。

Specific examples具体例子

  • A type constructor that cannot have a functor instance because the type cannot be implemented:由于无法实现类型而不能具有函子实例的类型构造函数

     data F za = F (a -> z)

This is a contrafunctor, not a functor, with respect to the type parameter a , because a in a contravariant position.对于类型参数a ,这是一个反函子,而不是函子,因为a处于逆变位置。 It is impossible to implement a function with type signature (a -> b) -> F za -> F zb .不可能实现具有类型签名(a -> b) -> F za -> F zb的函数。

  • A type constructor that is not a lawful functor even though the type signature of fmap can be implemented:即使可以实现fmap的类型签名,也不是合法函子的类型构造函数

     data Q a = Q(a -> Int, a) fmap :: (a -> b) -> Q a -> Q b fmap f (Q(g, x)) = Q(\\_ -> gx, fx) -- this fails the functor laws!

The curious aspect of this example is that we can implement fmap of the correct type even though F cannot possibly be a functor because it uses a in a contravariant position.这个例子的奇怪之处在于我们可以实现正确类型的fmap ,即使F不可能是一个函子,因为它在逆变位置使用a a。 So this implementation of fmap shown above is misleading - even though it has the correct type signature (I believe this is the only possible implementation of that type signature), the functor laws are not satisfied.因此, fmap显示的fmap实现具有误导性 - 即使它具有正确的类型签名(我相信这是该类型签名的唯一可能实现),但不满足函子定律。 For example, fmap idid , because let (Q(f,_)) = fmap id (Q(read,"123")) in f "456" is 123 , but let (Q(f,_)) = id (Q(read,"123")) in f "456" is 456 .例如fmap idid ,因为let (Q(f,_)) = fmap id (Q(read,"123")) in f "456"123 ,但是let (Q(f,_)) = id (Q(read,"123")) in f "456"456

In fact, F is only a profunctor, - it is neither a functor nor a contrafunctor.事实上, F只是一个 profunctor,——它既不是一个函子也不是一个反函子。

  • A lawful functor that is not applicative because the type signature of pure cannot be implemented: take the Writer monad (a, w) and remove the constraint that w should be a monoid.一个不适用的合法函子,因为不能实现pure的类型签名:取 Writer monad (a, w)并删除w应该是幺半群的约束。 It is then impossible to construct a value of type (a, w) out of a .它是那么不可能构造类型的值(a, w)的出a

  • A functor that is not applicative because the type signature of <*> cannot be implemented: data F a = Either (Int -> a) (String -> a) .一个不适用的函子,因为<*>的类型签名无法实现: data F a = Either (Int -> a) (String -> a)

  • A functor that is not lawful applicative even though the type class methods can be implemented:即使可以实现类型类方法,也不能合法应用的函子

     data P a = P ((a -> Int) -> Maybe a)

The type constructor P is a functor because it uses a only in covariant positions.类型构造函数P是一个函子,因为它只在协变位置使用a

instance Functor P where
   fmap :: (a -> b) -> P a -> P b
   fmap fab (P pa) = P (\q -> fmap fab $ pa (q . fab))

The only possible implementation of the type signature of <*> is a function that always returns Nothing : <*>类型签名的唯一可能实现是一个总是返回Nothing的函数:

 (<*>) :: P (a -> b) -> P a -> P b
 (P pfab) <*> (P pa) = \_ -> Nothing  -- fails the laws!

But this implementation does not satisfy the identity law for applicative functors.但是这个实现并不满足应用函子的恒等律。

  • A functor that is Applicative but not a Monad because the type signature of bind cannot be implemented.一个Applicative但不是Monad函子,因为无法实现bind的类型签名。

I do not know any such examples!我不知道任何这样的例子!

  • A functor that is Applicative but not a Monad because laws cannot be satisfied even though the type signature of bind can be implemented.一个Applicative但不是Monad函子,因为即使可以实现bind的类型签名,也无法满足法律。

This example has generated quite a bit of discussion, so it is safe to say that proving this example correct is not easy.这个例子引起了很多讨论,所以可以肯定地说,证明这个例子是正确的并不容易。 But several people have verified this independently by different methods.但是有几个人已经通过不同的方法独立验证了这一点。 See Is `data PoE a = Empty |请参阅是`数据PoE a = 空| Pair aa` a monad? 一对aa`一个monad? for additional discussion.以供进一步讨论。

 data B a = Maybe (a, a)
   deriving Functor

 instance Applicative B where
   pure x = Just (x, x)
   b1 <*> b2 = case (b1, b2) of
     (Just (x1, y1), Just (x2, y2)) -> Just((x1, x2), (y1, y2))
     _ -> Nothing

It is somewhat cumbersome to prove that there is no lawful Monad instance.证明不存在合法的Monad实例有点麻烦。 The reason for the non-monadic behavior is that there is no natural way of implementing bind when a function f :: a -> B b could return Nothing or Just for different values of a .非 monadic 行为的原因是,当函数f :: a -> B b可以为不同的a值返回NothingJustNothing实现bind自然方式。

It is perhaps clearer to consider Maybe (a, a, a) , which is also not a monad, and to try implementing join for that.考虑Maybe (a, a, a)可能更清楚,它也不是单子,并尝试为此实现join One will find that there is no intuitively reasonable way of implementing join .人们会发现没有一种直观合理的方式来实现join

 join :: Maybe (Maybe (a, a, a), Maybe (a, a, a), Maybe (a, a, a)) -> Maybe (a, a, a)
 join Nothing = Nothing
 join Just (Nothing, Just (x1,x2,x3), Just (y1,y2,y3)) = ???
 join Just (Just (x1,x2,x3), Nothing, Just (y1,y2,y3)) = ???
 -- etc.

In the cases indicated by ???在用???表示的情况下, it seems clear that we cannot produce Just (z1, z2, z3) in any reasonable and symmetric manner out of six different values of type a . ,很明显,我们不能从类型a的六个不同值中以任何合理和对称的方式产生Just (z1, z2, z3) We could certainly choose some arbitrary subset of these six values, -- for instance, always take the first nonempty Maybe - but this would not satisfy the laws of the monad.我们当然可以选择这六个值的任意子集,例如,总是取第一个非空的Maybe - 但这将不满足单子定律。 Returning Nothing will also not satisfy the laws.返回Nothing也不会满足的法律。

  • A tree-like data structure that is not a monad even though it has associativity for bind - but fails the identity laws.一个树状数据结构,它不是 monad,即使它具有bind关联性 - 但不符合恒等律。

The usual tree-like monad (or "a tree with functor-shaped branches") is defined as通常的树状单子(或“具有函子形分支的树”)被定义为

 data Tr f a = Leaf a | Branch (f (Tr f a))

This is a free monad over the functor f .这是一个基于函子f的自由单子。 The shape of the data is a tree where each branch point is a "functor-ful" of subtrees.数据的形状是一棵树,其中每个分支点都是子树的“函子”。 The standard binary tree would be obtained with type fa = (a, a) .标准二叉树将通过type fa = (a, a)

If we modify this data structure by making also the leaves in the shape of the functor f , we obtain what I call a "semimonad" - it has bind that satisfies the naturality and the associativity laws, but its pure method fails one of the identity laws.如果我们通过将叶子也做成函子f的形状来修改这个数据结构,我们就会得到我所说的“semimonad”——它具有满足自然性和结合律的bind ,但它的pure方法失败了其中一个身份法律。 "Semimonads are semigroups in the category of endofunctors, what's the problem?" “半单子是内函子范畴的半群,有什么问题?” This is the type class Bind .这是类型类Bind

For simplicity, I define the join method instead of bind :为简单起见,我定义了join方法而不是bind

 data Trs f a = Leaf (f a) | Branch (f (Trs f a))
 join :: Trs f (Trs f a) -> Trs f a
 join (Leaf ftrs) = Branch ftrs
 join (Branch ftrstrs) = Branch (fmap @f join ftrstrs)

The branch grafting is standard, but the leaf grafting is non-standard and produces a Branch .接枝是标准的,但接叶是非标准的,产生一个Branch This is not a problem for the associativity law but breaks one of the identity laws.这对于结合律来说不是问题,而是违反了恒等律之一。

When do polynomial types have monad instances?多项式类型什么时候有 monad 实例?

Neither of the functors Maybe (a, a) and Maybe (a, a, a) can be given a lawful Monad instance, although they are obviously Applicative .函子Maybe (a, a)Maybe (a, a, a)都不能被赋予合法的Monad实例,尽管它们显然是Applicative

These functors have no tricks - no Void or bottom anywhere, no tricky laziness/strictness, no infinite structures, and no type class constraints.这些函子没有技巧——任何地方都没有Voidbottom ,没有棘手的懒惰/严格,没有无限结构,也没有类型类约束。 The Applicative instance is completely standard. Applicative实例是完全标准的。 The functions return and bind can be implemented for these functors but will not satisfy the laws of the monad.函数returnbind可以为这些函子实现,但不满足 monad 的定律。 In other words, these functors are not monads because a specific structure is missing (but it is not easy to understand what exactly is missing).换句话说,这些函子不是 monad,因为缺少特定的结构(但并不容易理解到底缺少什么)。 As an example, a small change in the functor can make it into a monad: data Maybe a = Nothing | Just a举个例子,函子的一个小改动就可以使它变成一个 monad: data Maybe a = Nothing | Just a data Maybe a = Nothing | Just a is a monad. data Maybe a = Nothing | Just a是一个单子。 Another similar functor data P12 a = Either a (a, a) is also a monad.另一个类似的函子data P12 a = Either a (a, a)也是一个 monad。

Constructions for polynomial monads多项式单子的构造

In general, here are some constructions that produce lawful Monad s out of polynomial types.一般来说,这里有一些构造可以从多项式类型中产生合法的Monad In all these constructions, M is a monad:在所有这些结构中, M是一个 monad:

  1. type M a = Either c (w, a) where w is any monoid type M a = Either c (w, a)其中w是任何幺半群
  2. type M a = m (Either c (w, a)) where m is any monad and w is any monoid type M a = m (Either c (w, a))其中m是任何单子, w是任何幺半群
  3. type M a = (m1 a, m2 a) where m1 and m2 are any monads type M a = (m1 a, m2 a)其中m1m2是任何单子
  4. type M a = Either a (ma) where m is any monad type M a = Either a (ma)其中m是任何 monad

The first construction is WriterT w (Either c) , the second construction is WriterT w (EitherT cm) .第一个结构是WriterT w (Either c) ,第二个结构是WriterT w (EitherT cm) The third construction is a component-wise product of monads: pure @M is defined as the component-wise product of pure @m1 and pure @m2 , and join @M is defined by omitting cross-product data (eg m1 (m1 a, m2 a) is mapped to m1 (m1 a) by omitting the second part of the tuple):第三种构造是 monad 的组件方式乘积: pure @M定义为pure @m1pure @m2的组件方式乘积, join @M定义为省略交叉乘积数据(例如m1 (m1 a, m2 a)通过省略元组的第二部分映射到m1 (m1 a) ):

 join :: (m1 (m1 a, m2 a), m2 (m1 a, m2 a)) -> (m1 a, m2 a)
 join (m1x, m2x) = (join @m1 (fmap fst m1x), join @m2 (fmap snd m2x))

The fourth construction is defined as第四个结构定义为

 data M m a = Either a (m a)
 instance Monad m => Monad M m where
    pure x = Left x
    join :: Either (M m a) (m (M m a)) -> M m a
    join (Left mma) = mma
    join (Right me) = Right $ join @m $ fmap @m squash me where
      squash :: M m a -> m a
      squash (Left x) = pure @m x
      squash (Right ma) = ma

I have checked that all four constructions produce lawful monads.我已经检查过所有四种结构都产生了合法的 monad。

I conjecture that there are no other constructions for polynomial monads.猜想多项式单子没有其他构造。 For example, the functor Maybe (Either (a, a) (a, a, a, a)) is not obtained through any of these constructions and so is not monadic.例如,函子Maybe (Either (a, a) (a, a, a, a))不是通过任何这些构造获得的,因此不是一元的。 However, Either (a, a) (a, a, a) is monadic because it is isomorphic to the product of three monads a , a , and Maybe a .然而, Either (a, a) (a, a, a)是单子的,因为它同构于三个单子aaMaybe a的乘积。 Also, Either (a,a) (a,a,a,a) is monadic because it is isomorphic to the product of a and Either a (a, a, a) .此外, Either (a,a) (a,a,a,a)是一元的,因为它同构于aEither a (a, a, a)的乘积。

The four constructions shown above will allow us to obtain any sum of any number of products of any number of a 's, for example Either (Either (a, a) (a, a, a, a)) (a, a, a, a, a)) and so on.以上所示的四个构造将允许我们获得任何数量的任何数量的产品的任何总和a的,例如Either (Either (a, a) (a, a, a, a)) (a, a, a, a, a))等等。 All such type constructors will have (at least one) Monad instance.所有这样的类型构造函数都会有(至少一个) Monad实例。

It remains to be seen, of course, what use cases might exist for such monads.当然,对于此类 monad 可能存在哪些用例还有待观察。 Another issue is that the Monad instances derived via constructions 1-4 are in general not unique.另一个问题是通过结构 1-4 派生的Monad实例通常不是唯一的。 For example, the type constructor type F a = Either a (a, a) can be given a Monad instance in two ways: by construction 4 using the monad (a, a) , and by construction 3 using the type isomorphism Either a (a, a) = (a, Maybe a) .例如,类型构造函数type F a = Either a (a, a)可以通过两种方式被赋予一个Monad实例:通过使用 monad (a, a)构造 4,以及通过使用类型同构的构造 3 Either a (a, a) = (a, Maybe a) Again, finding use cases for these implementations is not immediately obvious.同样,为这些实现找到用例并不是很明显。

A question remains - given an arbitrary polynomial data type, how to recognize whether it has a Monad instance.一个问题仍然存在 - 给定任意多项式数据类型,如何识别它是否具有Monad实例。 I do not know how to prove that there are no other constructions for polynomial monads.我不知道如何证明多项式单子没有其他构造。 I don't think any theory exists so far to answer this question.我认为到目前为止还没有任何理论可以回答这个问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM