经常嗓子哑是什么原因| 男人都是大猪蹄子是什么意思| 高回声结节是什么意思| 木加号读什么| 泌尿科主要看什么病| 1979年出生属什么生肖| 突破性出血是什么意思| 谷子是什么意思| 血热吃什么药| 追什么| 唇色深是什么原因| 一年四季是什么生肖| 梦见买鞋子是什么意思| 痰多吃什么药| 闭角型青光眼是什么意思| 大便常规检查能查出什么| 6月3日是什么星座| 绝症是什么意思| 前列腺炎有什么症状表现| 朋友圈屏蔽显示什么| 阴虱卵长什么样图片| 频繁小便是什么原因| 物极必反什么意思| 早日康复是什么意思| 结婚唱什么歌送给新人| 哺乳期头痛可以吃什么药| 师长相当于地方什么级别| 工作效率是什么意思| 上嘴唇上有痣代表什么| 一什么凉席| 手抖挂什么科| 一什么图画| 什么是肾虚| 茶卡是什么意思| 卵巢黄体是什么意思| 正常人为什么会低血糖| 休闲裤配什么鞋子好看| 羽衣甘蓝是什么菜| 鳀鱼是什么鱼| 喉咙细菌感染吃什么药| cabbeen是什么牌子| 经期不能吃什么药| 膝盖发热是什么原因| 壬水代表什么| 老年人心跳过快是什么原因| 不劳而获是什么意思| 八月底什么星座| 七月八日是什么日子| 失恋什么意思| 碱性磷酸酶低是什么原因| 卤水点豆腐是什么意思| 甲沟炎是什么原因引起的| 幽门螺旋杆菌阳性是什么意思| xo酱是什么酱| 蒙奇奇是什么动物| 流产后吃什么食物| 收敛是什么意思| 剁椒鱼头属于什么菜系| 牵牛花又叫什么名字| 什么的蹲着| 慢性咽炎吃什么药效果最好| 阴沟肠杆菌是什么病| 火箭军是干什么的| 姑姑家的儿子叫什么| 血压低有什么危险| 尿中有泡沫是什么原因| 婴儿反复发烧是什么原因| 2004年是什么生肖| 狗狗胰腺炎有什么症状| 云吞面是什么面| 前列腺增大有什么危害| 什么人容易得妄想症| 泥鳅吃什么食物| 撕票是什么意思| 自食恶果是什么意思| 12.29是什么星座| 政字五行属什么| 什么动物吃蛇| 长期低血糖对人体有什么危害| 卵泡排出来是什么样的| 老气横秋是什么意思| 什么的舞动| 为什么母乳妈妈会便秘| 进击的巨人真相是什么| 冬阴功汤都放什么食材| mar是什么意思| 隙是什么意思| 天公作美是什么生肖| 愚人节是什么时候| 热休克蛋白90a检查高是什么原因| 长胸毛的男人代表什么| 灰指甲挂号挂什么科| 94年属什么今年多大| 男性乳头疼是什么原因| 睡觉开风扇有什么危害| youtube是什么软件| 男人吃什么补肾| 殳是什么意思| 脑血栓有什么症状| 甲沟炎用什么药膏好| 拉肚子是什么原因造成的| 左下眼皮跳是什么预兆| 手电筒的金属外壳相当于电路中的什么| 爸爸的爸爸叫什么儿歌| 决堤是什么意思| 白浆是什么| sakose是什么牌子| 黄芩有什么功效| 上善若水什么意思| 什么叫大数据| 属马的女生和什么属相最配| 年轮稀疏的一面是什么方向| 肠胃不舒服吃什么| 家里来狗是什么征兆| 积液是什么原因造成的怎么治疗| 鹿角菜是什么植物| 日昳是什么意思| 梦见发洪水是什么征兆| 促甲状腺激素高是什么意思| 20度穿什么衣服| 摩羯座哭了代表什么| 乳腺增生不能吃什么| 武夷山岩茶属于什么茶| 小孩干呕是什么原因| 什么是白内障症状| 脑白质病变是什么意思| 什么是商k| 795是什么意思| 增生是什么| 蛊虫是什么| 日木念什么| 樟脑是什么东西| 辄的意思是什么| 市政协秘书长是什么级别| hpv检查是什么| 回族女人为什么戴头巾| 背后长痘痘什么原因| cod表示什么| 领结婚证需要准备什么| 鸡的祖先是什么| 菊花茶有什么功效| 黄芪什么时候种植| 什么像什么又像什么| proof什么意思| 婴儿外阴粘连挂什么科| 肿物是什么意思| 月经推迟什么原因引起的| 唏嘘什么意思| geforce是什么牌子| 肝风内动是什么原因造成的| 什么的威尼斯| 静谧是什么意思| 耳聋是什么原因引起的| 什么是血铅| 5月13日什么星座| 白酒优级和一级有什么区别| 戊肝是什么病| 拂尘是什么意思| 炸膛什么意思| 阴道炎症用什么药| 捭阖是什么意思| 排卵日有什么症状| 血脂高是什么原因| 有潜力是什么意思| rhubarb是什么意思| 早餐吃什么最营养| 嗜血是什么意思| 小孩阑尾炎是由什么原因引起的| 什么鞋油好用| 1942年属什么生肖| 冲牛煞西是什么意思| 梦见掰玉米是什么意思| 这是什么石头| 胆结石是什么原因造成的| 格桑花是什么意思| 乳腺属于什么科室| 口苦尿黄是什么原因| 身上臭是什么原因| 约会去什么地方比较好| 黑眼圈是什么原因造成的| 什么样的细雨| 肾阳不足吃什么中成药| 砂锅是什么材料做的| asmr是什么意思| 贫血看什么科| 超敏c反应蛋白高是什么意思| 磨砂膏有什么作用| 鱼眼睛吃了有什么好处| 梦见手机屏幕摔碎了是什么意思| 牙痛安又叫什么| 西洋菜俗称叫什么| 13什么意思| 肚脐眼上面痛是什么原因引起的| 侏儒症是什么原因引起的| 肥胖纹长什么样| 梦见摘枣吃枣是什么意思| hcg值低是什么原因| 跟单员是做什么的| 公分的单位是什么| 梦见黑熊是什么预兆| 减肥可以吃什么肉| 婴儿为什么喜欢趴着睡| ph值是什么| 什么是真菌感染| 芭乐是什么季节的水果| 什么情况下做肾穿刺| 震楼神器楼上什么感觉| 输卵管堵塞是什么原因造成的| 盲肠憩室是什么意思| aimer是什么意思| 白带异常是什么原因| 人体7大营养素是什么| 精神洁癖是什么| 接骨木是什么| 七月二十二什么日子| 高压氧是什么| 贫血查什么| 2333是什么意思啊| 霸王花煲汤放什么材料| 陆家嘴为什么叫陆家嘴| 龙和什么相冲| 米鱼是什么鱼| 下体有异味是什么原因| 96年的鼠是什么命| 月经不来什么原因| 77年什么命| 一什么白菜| 夏吃姜有什么好处| 肝气犯胃吃什么中成药| 肝郁脾虚吃什么药效果最好| 维生素b补什么| 绝什么意思| 妇炎康片主要治什么妇科病| 身上长白斑是什么原因造成的| 正方形的纸能折什么| 吃梨有什么好处| 脖子疼什么原因| 婧是什么意思| 大便粘马桶是什么原因| 什么人容易得天疱疮| 四月28日是什么星座| 过生日吃什么菜寓意好| 谬论是什么意思| 眼角痒是什么原因| 梦见大白菜是什么意思| 孕妇吃火龙果有什么好处| 房早有什么危害| 男人吃什么补| 心机女是什么意思| 上市公司什么意思| 牛肉不能跟什么一起吃| 内消瘰疬丸主治什么病| thx是什么意思| 被蜜蜂蛰了涂什么药膏| 五谷丰收是什么生肖| 红薯是什么茎| 恰如其分是什么意思| 腊月是什么星座| 6.26是什么星座| 头发麻是什么原因| 吃完杏不能吃什么| 高筋面粉可以做什么| 失眠吃什么水果| rx是什么意思| 百度Jump to content

苹果也要自产OLED了 三星LG股价相应下跌

From Wikibooks, open books for an open world

Generalized algebraic datatypes, or simply GADTs, are a generalization of the algebraic data types that you are familiar with. Basically, they allow you to explicitly write down the types of the constructors. In this chapter, you'll learn why this is useful and how to declare your own.

We begin with an example of building a simple embedded domain specific language (EDSL) for simple arithmetical expressions, which is put on a sounder footing with GADTs. This is followed by a review of the syntax for GADTs, with simpler illustrations, and a different application to construct a safe list type for which the equivalent of head [] fails to typecheck and thus does not give the usual runtime error: *** Exception: Prelude.head: empty list.

Understanding GADTs

[edit | edit source]

So, what are GADTs and what are they useful for? GADTs are mainly used to implement domain specific languages, and so this section will introduce them with a corresponding example.

Arithmetic expressions

[edit | edit source]

Let's consider a small language for arithmetic expressions, given by the data type

data Expr = I Int         -- integer constants
          | Add Expr Expr -- add two expressions
          | Mul Expr Expr -- multiply two expressions

In other words, this data type corresponds to the abstract syntax tree, an arithmetic term like (5+1)*7 would be represented as (I 5 `Add` I 1) `Mul` I 7 :: Expr.

Given the abstract syntax tree, we would like to do something with it; we want to compile it, optimize it and so on. For starters, let's write an evaluation function that takes an expression and calculates the integer value it represents. The definition is straightforward:

eval :: Expr -> Int
eval (I n)       = n
eval (Add e1 e2) = eval e1 + eval e2
eval (Mul e1 e2) = eval e1 * eval e2

Extending the language

[edit | edit source]

Now, imagine that we would like to extend our language with other types than just integers. For instance, let's say we want to represent equality tests, so we need booleans as well. We augment the `Expr` type to become

data Expr = I Int
          | B Bool           -- boolean constants
          | Add Expr Expr
          | Mul Expr Expr
          | Eq  Expr Expr    -- equality test

The term 5+1 == 7 would be represented as (I 5 `Add` I 1) `Eq` I 7.

As before, we want to write a function eval to evaluate expressions. But this time, expressions can now represent either integers or booleans and we have to capture that in the return type

eval :: Expr -> Either Int Bool

The first two cases are straightforward

eval (I n) = Left n
eval (B b) = Right b

but now we get in trouble. We would like to write

eval (Add e1 e2) = eval e1 + eval e2  -- ???

but this doesn't type check: the addition function + expects two integer arguments, but eval e1 is of type Either Int Bool and we'd have to extract the Int from that.

Even worse, what happens if e1 actually represents a boolean? The following is a valid expression

B True `Add` I 5 :: Expr

but clearly, it doesn't make any sense; we can't add booleans to integers! In other words, evaluation may return integers or booleans, but it may also fail because the expression makes no sense. We have to incorporate that in the return type:

eval :: Expr -> Maybe (Either Int Bool)

Now, we could write this function just fine, but that would still be unsatisfactory, because what we really want to do is to have Haskell's type system rule out any invalid expressions; we don't want to check types ourselves while deconstructing the abstract syntax tree.

Exercise: Despite our goal, it may still be instructional to implement the eval function; do this.

Starting point:

data Expr = I Int
        | B Bool           -- boolean constants
        | Add Expr Expr
        | Mul Expr Expr
        | Eq  Expr Expr    -- equality test

eval :: Expr -> Maybe (Either Int Bool)
-- Your implementation here.

Phantom types

[edit | edit source]

The so-called phantom types are the first step towards our goal. The technique is to augment the Expr with a type variable, so that it becomes

data Expr a = I Int
            | B Bool
            | Add (Expr a) (Expr a)
            | Mul (Expr a) (Expr a)
            | Eq  (Expr a) (Expr a)

Note that an expression Expr a does not contain a value a at all; that's why a is called a phantom type, it's just a dummy variable. Compare that with, say, a list [a] which does contain a bunch of as.

The key idea is that we're going to use a to track the type of the expression for us. Instead of making the constructor

Add :: Expr a -> Expr a -> Expr a

available to users of our small language, we are only going to provide a smart constructor with a more restricted type

add :: Expr Int -> Expr Int -> Expr Int
add = Add

The implementation is the same, but the types are different. Doing this with the other constructors as well,

i :: Int  -> Expr Int
i = I
b :: Bool -> Expr Bool
b = B

the previously problematic expression

b True `add` i 5

no longer type checks! After all, the first argument has the type Expr Bool while add expects an Expr Int. In other words, the phantom type a marks the intended type of the expression. By only exporting the smart constructors, the user cannot create expressions with incorrect types.

As before, we want to implement an evaluation function. With our new marker a, we might hope to give it the type

eval :: Expr a -> a

and implement the first case like this

eval (I n) = n

But alas, this does not work: how would the compiler know that encountering the constructor I means that a = Int? Granted, this will be the case for all expressions that were created by users of our language because they are only allowed to use the smart constructors. But internally, an expression like

I 5 :: Expr String

is still valid. In fact, as you can see, a doesn't even have to be Int or Bool, it could be anything.

What we need is a way to restrict the return types of the constructors themselves, and that's exactly what generalized data types do.

GADTs

[edit | edit source]

To enable this language feature, add {-# LANGUAGE GADTs #-} to the beginning of the file.


The obvious notation for restricting the type of a constructor is to write down its type, and that's exactly how GADTs are defined:

data Expr a where
    I   :: Int  -> Expr Int
    B   :: Bool -> Expr Bool
    Add :: Expr Int -> Expr Int -> Expr Int
    Mul :: Expr Int -> Expr Int -> Expr Int
    Eq  :: Eq a => Expr a -> Expr a -> Expr Bool

In other words, we simply list the type signatures of all the constructors. In particular, the marker type a is specialised to Int or Bool according to our needs, just like we would have done with smart constructors.

And the great thing about GADTs is that we now can implement an evaluation function that takes advantage of the type marker:

eval :: Expr a -> a
eval (I n) = n
eval (B b) = b
eval (Add e1 e2) = eval e1 + eval e2
eval (Mul e1 e2) = eval e1 * eval e2
eval (Eq  e1 e2) = eval e1 == eval e2

In particular, in the first case

eval (I n) = n

the compiler is now able to infer that a=Int when we encounter a constructor I and that it is legal to return the n :: Int; similarly for the other cases.

To summarise, GADTs allows us to restrict the return types of constructors and thus enable us to take advantage of Haskell's type system for our domain specific languages. Thus, we can implement more languages and their implementation becomes simpler.

Summary

[edit | edit source]

Syntax

[edit | edit source]

Here's a quick summary of how the syntax for declaring GADTs works.

First, consider the following ordinary algebraic datatypes: the familiar List and Maybe types, and a simple tree type, RoseTree:

Maybe List Rose Tree
data Maybe a =  
    Nothing |   
    Just a
data List a = 
    Nil |  
    Cons a (List a)
data RoseTree a = 
     RoseTree a [RoseTree a]

Remember that the constructors introduced by these declarations can be used both for pattern matches to deconstruct values and as functions to construct values. (Nothing and Nil are functions with "zero arguments".) We can ask what the types of the latter are:

Maybe List Rose Tree
> :t Nothing
Nothing :: Maybe a
> :t Just
Just :: a -> Maybe a  
> :t Nil
Nil :: List a
> :t Cons
Cons :: a -> List a -> List a    
> :t RoseTree
RoseTree ::
   a -> [RoseTree a] -> RoseTree a    

It is clear that this type information about the constructors for Maybe, List and RoseTree respectively is equivalent to the information we gave to the compiler when declaring the datatype in the first place. In other words, it's also conceivable to declare a datatype by simply listing the types of all of its constructors, and that's exactly what the GADT syntax does:

Maybe List Rose Tree
data Maybe a where
   Nothing  :: Maybe a
   Just :: a -> Maybe a
data List a where
   Nil  :: List a
   Cons :: a -> List a -> List a
data RoseTree a where 
   RoseTree ::
      a ->  [RoseTree a] -> RoseTree a

This syntax is made available by the language option {-#LANGUAGE GADTs #-}. It should be familiar to you in that it closely resembles the syntax of type class declarations. It's also easy to remember if you already like to think of constructors as just being functions. Each constructor is just defined by a type signature.

New possibilities

[edit | edit source]

Note that when we asked the GHCi for the types of Nothing and Just it returned Maybe a and a -> Maybe a as the types. In these and the other cases, the type of the final output of the function associated with a constructor is the type we were initially defining - Maybe a, List a or RoseTree a. In general, in standard Haskell, the constructor functions for Foo a have Foo a as their final return type. If the new syntax were to be strictly equivalent to the old, we would have to place this restriction on its use for valid type declarations.

So what do GADTs add for us? The ability to control exactly what kind of Foo you return. With GADTs, a constructor for Foo a is not obliged to return Foo a; it can return any Foo blah that you can think of. In the code sample below, for instance, the MkTrueGadtFoo constructor returns a TrueGadtFoo Int even though it is for the type TrueGadtFoo a.

Example: GADT gives you more control:

data FooInGadtClothing a where
 MkFooInGadtClothing :: a -> FooInGadtClothing a

--which is no different from:  data Haskell98Foo a = MkHaskell98Foo a ,

--by contrast, consider:
 
data TrueGadtFoo a where
  MkTrueGadtFoo :: a -> TrueGadtFoo Int

--This has no Haskell 98 equivalent.

But note that you can only push the generalization so far... if the datatype you are declaring is a Foo, the constructor functions must return some kind of Foo or another. Returning anything else simply wouldn't work.

Example: Try this out. It doesn't work:

data Bar where
  BarNone :: Bar -- This is ok

data Foo where
  MkFoo :: Bar Int-- This will not typecheck

Examples

[edit | edit source]

Safe Lists

[edit | edit source]
Prerequisite: We assume in this section that you know how a List tends to be represented in functional languages
Note: The examples in this section additionally require the extensions EmptyDataDecls and KindSignatures to be enabled

We've now gotten a glimpse of the extra control given to us by the GADT syntax. The only thing new is that you can control exactly what kind of data structure you return. Now, what can we use it for? Consider the humble Haskell list. What happens when you invoke head []? Haskell blows up. Have you ever wished you could have a magical version of head that only accepts lists with at least one element, lists on which it will never blow up?

To begin with, let's define a new type, SafeList x y. The idea is to have something similar to normal Haskell lists [x], but with a little extra information in the type. This extra information (the type variable y) tells us whether or not the list is empty. Empty lists are represented as SafeList x Empty, whereas non-empty lists are represented as SafeList x NonEmpty.

-- we have to define these types
data Empty
data NonEmpty

-- the idea is that you can have either 
--    SafeList a Empty
-- or SafeList a NonEmpty
data SafeList a b where
-- to be implemented

Since we have this extra information, we can now define a function safeHead on only the non-empty lists! Calling safeHead on an empty list would simply refuse to type-check.

safeHead :: SafeList a NonEmpty -> a

So now that we know what we want, safeHead, how do we actually go about getting it? The answer is GADT. The key is that we take advantage of the GADT feature to return two different list-of-a types, SafeList a Empty for the Nil constructor, and SafeList a NonEmpty for the Cons constructor:

data SafeList a b where
  Nil  :: SafeList a Empty
  Cons :: a -> SafeList a b -> SafeList a NonEmpty

This wouldn't have been possible without GADT, because all of our constructors would have been required to return the same type of list; whereas with GADT we can now return different types of lists with different constructors. Anyway, let's put this all together, along with the actual definition of SafeHead:

Example: safe lists via GADT

{-#LANGUAGE GADTs, EmptyDataDecls #-}
-- (the EmptyDataDecls pragma must also appear at the very top of the module,
-- in order to allow the Empty and NonEmpty datatype declarations.)

data Empty
data NonEmpty

data SafeList a b where
     Nil :: SafeList a Empty
     Cons:: a -> SafeList a b -> SafeList a NonEmpty

safeHead :: SafeList a NonEmpty -> a
safeHead (Cons x _) = x

Copy this listing into a file and load in ghci -fglasgow-exts. You should notice the following difference, calling safeHead on a non-empty and an empty-list respectively:

Example: safeHead is... safe

Prelude Main> safeHead (Cons "hi" Nil)
"hi"
Prelude Main> safeHead Nil

<interactive>:1:9:
    Couldn't match `NonEmpty' against `Empty'
      Expected type: SafeList a NonEmpty
      Inferred type: SafeList a Empty
    In the first argument of `safeHead', namely `Nil'
    In the definition of `it': it = safeHead Nil

The complaint is a good thing: it means that we can now ensure during compile-time if we're calling safeHead on an appropriate list. However, that also sets up a pitfall in potential. Consider the following function. What do you think its type is?

Example: Trouble with GADTs

silly False = Nil
silly True  = Cons () Nil

Now try loading the example up in GHCi. You'll notice the following complaint:

Example: Trouble with GADTs - the complaint

Couldn't match `Empty' against `NonEmpty'
     Expected type: SafeList () Empty
     Inferred type: SafeList () NonEmpty
   In the application `Cons () Nil'
   In the definition of `silly': silly True = Cons () Nil

The cases in the definition of silly evaluate to marked lists of different types, leading to a type error. The extra constraints imposed through the GADT make it impossible for a function to produce both empty and non-empty lists.

If we are really keen on defining silly, we can do so by liberalizing the type of Cons, so that it can construct both safe and unsafe lists.

Example: A different approach

{-#LANGUAGE GADTs, EmptyDataDecls, KindSignatures #-}
-- here we add the KindSignatures pragma,
-- which makes the GADT declaration a bit more elegant.

-- Note the subtle yet revealing change in the phantom type names.
data NotSafe
data Safe


data MarkedList             ::  * -> * -> * where
  Nil                       ::  MarkedList t NotSafe
  Cons                      ::  a -> MarkedList a b -> MarkedList a c


safeHead                    ::  MarkedList a Safe -> a
safeHead (Cons x _)          =  x

-- This function will never produce anything that can be consumed by safeHead,
-- no matter that the resulting list is not necessarily empty.
silly                       :: Bool -> MarkedList () NotSafe
silly False                  =  Nil
silly True                   =  Cons () Nil

There is a cost to the fix above: by relaxing the constraint on Cons we throw away the knowledge that it cannot produce an empty list. Based on our first version of the safe list we could, for instance, define a function which took a SafeList a Empty argument and be sure anything produced by Cons would not be accepted by it. That does not hold for the analogous MarkedList a NotSafe; arguably, the type is less useful exactly because it is less restrictive. While in this example the issue may seem minor, given that not much can be done with an empty list, in general it is worth considering.


Exercises
  1. Could you implement a safeTail function? Both versions introduced here would count as valid starting material, as well as any other variants in similar spirit.

A simple expression evaluator

[edit | edit source]
Insert the example used in Wobbly Types paper... I thought that was quite pedagogical
This is already covered in the first part of the tutorial.

Discussion

[edit | edit source]
More examples, thoughts
From FOSDEM 2006, I vaguely recall that there is some relationship between GADT and the below... what?

Phantom types

[edit | edit source]

See Phantom types.

Existential types

[edit | edit source]

If you like Existentially quantified types, you'd probably want to notice that they are now subsumed by GADTs. As the GHC manual says, the following two type declarations give you the same thing.

data TE a = forall b. MkTE b (b->a)
data TG a where { MkTG :: b -> (b->a) -> TG a }

Witness types

[edit | edit source]
儿童肚子痛吃什么药 吃饭时头晕是什么原因 眼睛红是什么原因引起的 10月底是什么星座 侯亮平是什么级别
今天拉警报什么意思 什么是单反相机 肠痈是什么意思 卵巢是什么 259是什么意思
盐水泡脚有什么好处 为什么可乐能溶解鱼刺 掉头发是缺什么维生素 红糖是什么做的 1940年属什么生肖
两岁宝宝不开口说话是什么原因 脂肪球是什么意思 02年属马的是什么命 子宫动脉阻力高是什么引起的 喝苹果醋有什么好处和坏处
下身瘙痒什么原因shenchushe.com 晚餐吃什么菜谱大全hcv9jop2ns4r.cn 松鼠鱼是什么鱼hcv8jop1ns4r.cn 放下是什么意思hcv8jop6ns5r.cn 吃晕车药有什么副作用hcv9jop4ns0r.cn
锁骨疼是什么原因hcv9jop1ns9r.cn 小肠疝气挂什么科hcv8jop5ns0r.cn 最好的大学是什么大学hcv7jop9ns8r.cn 京东自营店什么意思hcv9jop4ns5r.cn 吃什么容易排大便hcv7jop4ns8r.cn
熤是什么意思hcv8jop4ns7r.cn 奶茶和奶绿有什么区别hcv8jop7ns1r.cn 来例假肚子疼吃什么药520myf.com 八哥吃什么hcv9jop3ns9r.cn 榴莲不可以和什么食物一起吃hcv8jop4ns5r.cn
餐后血糖高吃什么药hcv9jop2ns8r.cn 母的第三笔是什么hcv8jop8ns9r.cn 软文什么意思hcv7jop5ns1r.cn 什么症状hcv9jop5ns3r.cn 青衣是什么意思hcv7jop7ns1r.cn
百度