http指南单子版

重要要点

  • 避免显式处理有状态值是值得的。
  • 使用monad,可以从代码中删除对有状态值的显式处理。
  • 为了符合Monad类型,类型必须具有与之关联的特定类型的功能(称为“绑定”)。
  • 使用monad的bind函数,有状态值会从一个monad传递到另一个monad,始终留在monad内部(而不是显式处理)。
  • 使用monad可以解决许多不同类型的问题。

随着当前函数编程的爆炸式增长,“ monad”函数结构再次使新人心生恐惧。 莫纳德人起源于数学中的范畴论领域,并于1990年代被引入编程语言,它是诸如Haskell之类的纯函数语言的基本结构。

以下是大多数新手对单子的了解:

  • 一个monad对进行输入和输出很有用。
  • monad除输入和输出外,还可以用于其他方面。
  • 一个monad很难理解,因为有关monads的大多数文章都讲得太多或太少。

第三个项目符号激励我写这篇文章-这篇向读者介绍单子词的文章(甚至是千篇)。 运气好的话,您将在这篇文章的结尾处感到单子并不那么可怕。

系统状态

在计算机程序中,“状态”一词描述了全局变量,输入,输出以及特定功能以外的任何其他内容。 关于程序状态,需要记住以下几点:

  • 它的值从一个功能的执行一直持续到另一个功能。
  • 它可用于多个功能。
  • 它可以是可变的。 如果是这样,则其值与时间有关。

状态很难管理,因为没有单个函数拥有状态。 考虑这种情况:

  • 功能编号1从状态中获取一个值,并在执行其指令时使用该值。 同时,功能编号2修改了该值。 结果,功能编号1根据最新的值做出决策。
  • 更糟的是,功能编号1使用其过时的数据执行计算,然后将状态值替换为新的不准确的值。 现在,功能编号1的错误已变得具有传染性,从功能内部传播到整个全局系统。

无论如何,系统状态是时间的函数,所以时间是我们必须担心的额外维度。 我们真的不能问“ x的值是多少?” 相反,我们必须问“在时间tx的值是多少?”。 这增加了复杂性,使代码难以推理。 所以最重要的是...

表情和动作

表达式是具有值的一段文本。 例如,考虑以下代码:

x = 5
y = x + 7
x = y + 1

第一次出现的x是具有值5的最后出现的表达式x是与该代码包含其他表达式的值13的表达式。 例如,在中间一行, x + 7是一个值为12的表达式。

在大多数计算机语言中,从键盘读取的命令是一个表达式,并且该表达式具有值。 考虑以下语句:

x = nextInput()

您会在Java,C ++和许多其他编程语言中找到这种语句。 现在,假设用户键入数字5 。 然后nextInput()是一个值为5的表达式。 语句的执行将nextInput()表达式的值(值5 )分配给变量x 。 如果x是程序状态的一部分,则此语句修改该状态。 正如我们已经看到的那样,修改系统状态可能很危险。

在我们的nextInput()例如,值nextInput()是依赖于时间的。 第一次执行nextInput()nextInput()表达式的值可能为5。稍后,当您第二次执行nextInput()时,相同表达式的值可能为17。在松散类型的语言中, nextInput()可能从5更改为"Hello, world"

为了消除时间依赖性,我们停止使用nextInput()并将其替换为我将称为doInput的函数。 作为表达式, doInput()值不是517"Hello, world" 。 相反, doInput()的值是一个操作。 此操作在运行时从键盘获取输入。

一个动作可能会或可能不会发生。 从键盘读取是一个动作。 在屏幕上写"Hello, world"是一个动作。 打开在"/Users/barry/myfile.txt"找到的文件是一项操作。 建立与http://www.infoq.com的连接是一项操作。 动作是某种计算。 通常,动作的详细信息不会在程序的源代码中详细说明。 相反,动作是运行时现象。

在许多语言中,当您考虑类型时,就会想到整数,浮点数,字符串,布尔值以及其他类似的东西。 您可能不会将动作视为一种类型。 但是,当我们执行monadic I / O时,诸如doInput()之类的表达式的值就是一个动作。 doInput()doInput()调用返回的值是动作类型。

用这种思维方式, doInput()的值与时间无关。 无论表达式doInput()在程序中出现的位置如何,该表达式始终具有相同的值。 它的值是从键盘获得输入的动作。

无论是州还是州,我们都取得了进步。

您不能用动作值做很多事情

我们有一个小问题。 我们想对一个值是一个动作的表达式做一些有用的事情。 我们该怎么做? 如果某个动作从键盘上获得数字5,则无法将1加到该动作上。

x = doInput()
print(x + 1)

在与语言无关的代码中,变量x表示动作。 动作是一种类型,值1是完全不同的类型。 因此,表达式x + 1仅比在重新启动计算机的操作中添加1有意义。 表达式restart + 1是胡说!

如果您不想在用户输入中加1 ,该怎么办? 现在下面的代码有意义吗?

x = doInput()
print(x)

不,不是。 语句x = doInput()x分配了一个动作,因此语句print(x)试图显示一个动作。 当您尝试在计算机屏幕上显示动作时,它看起来像什么?

您可能会争辩说,使用弱类型语言,您可以模糊输入操作和它们从键盘获得的值之间的差异。 就像2 == "2"在JavaScript中是正确的一样, 5 == the_action_obtaining_5在某些弱类型函数语言中也可能是正确的。 但是在the_action_obtaining_5需要某种处理才能从the_action_obtaining_5获得5 。 此外,如果您失去了5the_action_obtaining_5之间的区别,那么doInput()和原始的nextInput()之间几乎没有什么区别; 返回到以输入函数调用为时间依赖表达式的地方。 那不是你想要的地方。

建立一条链

因此,情况很明显; 我们无法显示doInput()表达式的值。 但是,如果我们可以巧妙地链接动作并在doInput()动作之后执行另一个要打印的动作,该怎么办? 亲爱的读者,那将是一个单子。

doInput()操作与诸如5 (用户在键盘上输入的值)之类的值有关。 当我们处理doInput() ,我们最感兴趣的是用户键入的数字5 ,而不是doInput()操作本身。

让我们仔细看看。 如果程序跟踪仓库中的库存,则输入数字5可能表示货架上的箱子数。 值5与问题域有关。 如果您走到管理仓库的人那里并说“您有5个盒子”,则该人会明白​​您的意思。 另一方面, doInput()表达式的值是对库存管理人员没有意义的动作。 doInput()操作是我们处理库存方式的doInput() 。 因此,在本文中,我将与动作相关的相关值与动作本身区分开来。 为了强调动作缺乏针对性,我将动作本身称为工件

回顾一下。 当您执行doInput()并且用户输入数字5时,

  • 接收输入的动作就是我们所说的工件
  • 数字5是我们称为相关值的数字

这种相关的/伪造的术语无法与每个monad很好地配合使用,但是它可以帮助我解释monad的全部含义。 为了提供更多帮助,在本文的大多数示例中,我都将相关值设为整数值。

如果我很草率,我认为doInput()动作是一个具有相关值的容器,例如内部有5冒泡。 容器隐喻很有用,因为一旦将值与monad相关联,我们就希望将该值附加到monad上。 我们不喜欢让价值从其monad容器中泄漏出去。 请记住,当您认真对待Monad时,这些相关性和容器类比会崩溃。 即使这样,这些类比对于建立有关单子的直觉还是非常有用的。

因此,这是我们面临的挑战:我们必须形式化使用与doInput()操作相关的相关用户输入的方式。 (在本文中,“正式化”一词的意思不是“绝对严格。”而是“用简单的英语简要描述来使内容更精确”。)

有些类型是单子。 其他不是

在典型的编程语言中,您可能具有整数类型,浮点类型,布尔类型,字符串类型以及许多不同类型的复合类型。 您可以将一种类型描述为单子类型,也可以将其描述为单子类型。 monad类型是哪种类型?

对于初学者,单子类型具有相关的值或与之关联的许多相关值。 以下是一些具有相关值的类型的示例:

  1. I / O操作类型:
    对于键盘输入操作,相关值是用户输入的值。 工件是动作本身。
  2. 列表类型:
    想象一个值列表:[3,17,24,0,1]。 在此列表中,相关的值为3、17、24、0和1。工件是事实,这些值被收集以形成一个列表。 如果您不喜欢列表,可以考虑数组,向量或您喜欢的编程语言中的任何其他集合结构。
  3. 也许类型:
    空值在许多语言中都是有问题的,并且函数式编程有一种解决方法。 在我的简化方案中,Maybe值包含数字(例如5)或Nothing指示符。 如果计算无法确定值,则可能包含Nothing而不是值。

    Java和Swift之类的语言具有Optional类型。 Optional与我们的Maybe类型相似。

    对于产生Maybe值的计算,相关值可以是数字或特殊的Nothing指示符。 工件是一个概念,即计算的结果不仅仅是一个数字。

  4. 作家类型:
    Writer是一项功能,作为其工作的一部分,它会生成一些写入到正在进行的日志中的信息。 想象一下,例如,一个带有附加值的花式平方函数:

    square(6) = (36, "false")

    数字366*6 。 单词"false"表示答案36不能被10整除。在应用了不同Writer函数的多个应用程序之后,日志可能看起来像这样:

    "false true false"

    在此示例中,相关值是数字结果,例如36 。 工件是字符串值( "true""false" ),并将数字结果与字符串值捆绑在一起。

对于每种类型,正式使用该类型的相关值会带来一些挑战。 特别是:

  1. 对于“ I / O操作”类型:
    您有一个动作,并且某些用户输入与此动作相关联。 您不能只为该操作加1,也不能在计算机屏幕上回显该操作。 动作类型不正确。 您必须定义如何使用动作的相关值来做某事。
  2. 对于列表类型:
    你有一个带有数字的清单。 在我们的示例中,其中包含3、17、24、0和1。 您不能对列表本身进行数值计算。 列表的类型不正确。 您必须定义如何使用列表中的每个数字来执行某项操作(例如“ +1”)。
  3. 对于Maybe类型:
    想象两个名为maybe1maybe2 Maybe值。 maybe1Nothing关联,而maybe2值有5关联。 maybe1值来自某个不成功的计算,可能的maybe2值来自某个计算,其中5是结果。

    你可以添加1maybe1价值? 号的Nothing值与相关联的maybe1 ,所以1 + maybe1是无意义的。

    可以将maybe2值加1吗? 为了了解单子,我的答案仍然是“否”。 5值与maybe2相关联,但maybe25不相同。 maybe2值不是一个数字-这是一个与5关联的构件结构。 所以1 + maybe2是没有意义的。

    缺少的链接是,您必须定义如何使用与Maybe值关联的相关值来做某事。

  4. 对于Writer类型:
    从一些帮助您找到((6 * 6)+ 4)/ 5的普通函数开始。

    square(6) = 36
    plus4(36) = 40
    dividedBy5(40) = 8

    您可以将这些功能链接在一起以获得所需的结果:

    dividedBy5(plus4(square(6))) = 8

    但是,如果每个函数都是一个Writer,并且每个函数的结果都包含一个可被10整除的指标,那么情况就不一样了:

累积的日志包含:

square(6) = (36, "false") "false"

plus4(36) = (40, "true") "false true"

dividedBy5(40) = ( 8, "false") "false true false"

您不能将plus4函数应用于square函数返回的对。

plus4(square(6))

plus4((36, "false")这是没有意义的

取而代之的是,您将plus4函数应用于square函数执行中的相关值。 同样,在应用dividedBy5函数从相关值plus4功能的执行。

使用一个简单的规则,您可以一次全部确定形式,一种将每个函数调用应用于上一个调用的相关值的方式。 这使我们进入了bind功能。

要使类型成为Monad,它必须具有绑定功能

在大多数编程语言中,类型都有自己的功能。 例如,整数类型具有+,-,*和/函数。 字符串类型具有其串联功能。 布尔类型具有其or,and和not函数。

要成为monad,一个类型必须具有使用monad的相关值或多个值的函数,并且该函数必须具有特定的形式。 让我们看看这种形式是什么。

函数形式(错误观念)的第一个候选对象是函数(我将其称为badIdea ),用于将相关值与monad隔离。 例如, badIdea函数可能会从操作中获取用户的输入。 如果调用doInput()且用户键入数字5 ,则badIdea(doInput())5 。 应用badIdea函数后,可以打印badIdea()调用的结果值。 您甚至可以将1添加到badIdea()调用的结果值中。

x = badIdea(doInput())
print(x)
y = x + 1

现在您回到了开始的地方,然后提出了nextInput()函数的时间依赖性问题。 表达式badIdea(doInput())具有原始nextInput()函数的所有不良功能。 一次执行badIdea(doInput()) ,作为表达式,其值可能为5badIdea(doInput())执行badIdea(doInput()) ,其值可能变为17 。 在松散类型的语言中, badIdea(doInput())可能从5变为"Hello, world"

使用badIdea函数,您可以从doInput()操作中获取相关值,并使用该值执行任何所需的操作。 让我们从doInput()操作中获取相关的值, 然后创建另一个使用该值执行有用操作的操作 ,而不是实施这个坏主意。 这很重要:您在doInput()操作之后执行另一个操作。 当您将此思想形式化时,动作类型将变为单子类型。 因此,让我们开始正式化想法:

我们从两件事开始:一个动作和一个功能。 例如,

  • 动作doInput()从键盘获取一个数字。
  • 函数doPrint接受一个数字,并产生一个在屏幕上写入该数字的动作。

doPrint(5) =在屏幕上写入5的动作
doPrint ( 19) =在屏幕上写入19的动作

图1说明了这种情况。 在此图中,每个齿轮图代表某种动作

我可以将doPrint描述为从数量到动作的函数。 当您执行函数式编程时,此类函数自然会出现。

让我们在这里暂停一下我的说法; 考虑两个表达式,如opSystem没有括号,并opSystem()用括号。 opSystem表达式的值是一个特定的函数-该函数可以伸出并发现当前正在运行的操作系统。 但是opSystem()表达式的值是一个名称-类似于"Linux""Windows 10" 。 简而言之,

  • opSystem代表功能,并且
  • opSystem()代表从调用opSystem函数返回的值。

考虑到这一点,请注意带括号的“ action doInput() ”与不带括号的“ doPrint函数”之间的区别。 doInputdoPrint函数均返回值,并且在两种情况下,这些值均为操作。 当我写“ action doInput() ”时,我指的是doInput()函数调用返回的doInput() 。 当我写“函数doPrint ”时,我指的是doPrint函数本身,而不是函数的返回值。 这说明了我在图1中说明doInput()doPrint的方式上的差异。为了说明doInput() ,我画了一个齿轮,该齿轮应该使您想起一个动作。 为了说明doPrint ,我画了一个箭头,它使您想起一个功能。 当您在考虑单子时,这有助于使此类问题始终处于您的思考重点。

通过doInput()动作和doPrint函数,我们还需要一件来制作单子。 我们需要一种将doInput()doPrint在一起的方法。 更准确地说,我们需要一个公式来执行任何动作A ,以及任何从数到动作函数f ,并将它们组合以创建一个新动作。 我们将此公式命名为bind 。 见图2。

在上一段中,我称bind为公式,但bind实际上是另一个函数。

bind(A,f) =某种动作

bind函数是一个高阶函数,因为bind函数将一个函数作为其参数之一。 如果您不习惯考虑函数的功能,则bind函数会使您的头脑混乱。

对于输入和输出, bind函数必须是一条通用规则,该规则将任何I / O操作A和任何从编号到操作功能f用作参数 bind函数必须返回一个新的I / O操作。 例如:

  • doInput() =从键盘获取数字的动作
    doPrint(x) =在屏幕上写入x值的动作

bind(doInput(),doPrint) =
在屏幕上写入doInput()相关值的操作

参见图3。

让我们稍微更改一下示例:

  • doPrintPlus1(x) =在屏幕上写入x+1值的动作

    bind(doInput(),doPrintPlus1) =
    在屏幕上写入( doInput()相关值)+1的动作

参见图4。

通常,对于输入/输出动作A以及从数量到动作函数f

bind(A,f) =f应用于A的相关值的动作

参见图5。

如果您发现bind的描述令人困惑,那么您并不孤单。 高阶函数不容易理解。

在Haskell编程语言中, bind函数起着如此重要的作用,在该语言中内置了bind运算符>>= 。 实际上,许多用于处理monad的功能都直接引入了Haskell。

不再依赖时间

让我们重新访问图3所示的doInput(),doPrint场景。

bind(doInput(),doPrint) =
在屏幕上写入doInput()相关值的操作

bind(doInput(),doPrint)表达式的任何部分bind(doInput(),doPrint)具有与时间相关的值。 无论此表达式何时出现在您的代码中,

  • doInput()始终是相同的动作-从键盘获取数字的动作。
  • doPrint始终是相同的从编号到动作功能。
  • 整个表达式bind(doInput(),doPrint)始终是相同的动作-将数字写入屏幕的动作。

用户在键盘上键入的值从一瞬间变为下一瞬间,但是bind(doInput(),doPrint)表达式的任何部分都不代表该值。 我们还没有消除所有副作用,但是我们已经消除了代码中对系统状态的任何明确提及。 用户在键盘上输入数字后,该数字就像烫手山芋一样从一个动作传递到另一个动作。 代码的变量均不代表用户键入的值。

并非总是与数字和输入/输出操作有关

在到目前为止的示例中,

  • doInput()的相关值是一个数字,而doPrint的参数类型也是一个数字。
  • doInput()的类型是一个动作,而doPrint的返回类型是一个动作。

bind函数告诉您如何组合doInput()doPrint以获得全新的操作。

有些单子与数字或输入/输出操作没有任何关系。 因此,让我们以稍微更一般的方式doPrintdoInputdoPrintbind函数的观察:

  • doPrint的参数类型与doInput()的相关值类型相同。
  • doPrint的返回类型与doInput()的类型相同。

bind函数告诉您如何组合doInput()doPrint以获得全新的价值。 该新值与原始doInput()值的类型相同。

更多单子类型

任何具有绑定函数(以及我将在本文末尾描述的其他函数)的类型都是monad。 通过前面几节中介绍的输入/输出绑定函数,输入/输出操作类型变为monad。 我们之前介绍的列表,Maybe和Writer类型也是monad类型。 这是列表类型的工作方式:

从两件事开始:一个列表L和一个函数f 。 同样,函数f是某种类型的。

  • 作为参数, f接受一个值,该值的类型与列表中的元素相同。
  • 对于其结果, f产生一个列表。

如果列表包含数字,则f从数字到列表的函数。 这是一个公式( bind的定义),该公式采用任何列表和任何从编号到列表的功能,并将它们组合以创建新列表:

bind(L,f) =通过将f应用于每个列表而获得的新列表
L的元素,然后展平结果列表列表

让我们看一个例子:

  • f(x) =列表[squareRoot(x),-squareRoot(x)]

    根据需要, f是一个从数字到列表的函数。

    L = [4,25,81]

    根据我们对列表bind的定义:

    bind(L,f) = [[2,-2], [5,-5], [9,-9]]

    = [2,-2,5,-5,9,-9]

bind函数为您提供了将函数f应用于列表L任何元素的所有可能结果。 参见图6。

  • 这是一个不涉及数字的示例。 令x为一本书,令f(x) =该书的作者列表

    例如,

    f(C_Programming_Lang) = [Kernighan,Ritchie]

    在此示例中, f是从书到表的功能。 根据我们对列表bind的定义:

    bind([C_Programming_Lang, Design_Patterns], f) =

    扁平化
    [[Kernighan,Ritchie],[Gamma,Helm,Johnson,Vlissides]] =
    [Kernighan,Ritchie,Gamma,Helm,Johnson,Vlissides]

我们已经设法从书籍清单中获得了作者名单。 参见图7。

注意图2至图7之间的相似之处:

  • 在图的左侧,您有一个monad值(例如,一个动作,一个列表)和一个函数。 该函数采用相关值并产生单值。
  • 左中角有一个箭头,指示bind功能的应用。
  • 在图的右侧,您具有全新的monad值。

Maybe类型的bind函数如何? 令m为Maybe值(包含数字或Nothing),令f为接受数字并产生Maybe值的函数。 bind(m,f)的值取决于Maybe值内的内容:

bind(m,f) =要么
f(m's pertinent value)如果m不包含任何内容
要么
如果m包含任何内容

让我们列出一些例子。

maybe1包含任何东西
maybe2包含5
maybe3包含0
f(x) =可能包含100 / x

然后

bind(maybe1,f) =可能不包含Nothing
bind(maybe2,f) =可能包含20的值
bind(maybe3,f) =可能不包含Nothing

同样, bind函数的参数是monad(在这种情况下为Maybe值)和函数。 该函数的参数是monad的相关值,并且该函数返回另一个monad。

当然,我们可以为Writer monad创建一个bind函数。

bind((aNumber,aString), f) =

( f(aNumber), aString+的数字部分f(aNumber), aString+的字符串部分f(aNumber))

一些示例可以帮助您理解这一点。 回忆我们的squareplus4dividedBy5函数-这些函数的返回值包括一个10整除指标。

bind((36, "false"), plus4) = (40, "false true")参见图8。

bind((40, "false true"), dividedBy5) = (8, "false true false")

当您应用bind ,结果的字符串部分包括来自先前计算的字符串。 字符串部分是所有计算的运行日志。

Monad需要一项附加功能

bind函数可能不容易理解,但是monad必须具有的另一个函数非常简单。 我称它为toMonad函数。

  • toMonad函数将相关值作为其参数。
  • toMonad函数返回一个monad。

粗略地说, toMonad返回最简单的monad,它可能包含给定的相关值。 (有一个涉及toMonadbind交互的更精确的定义,但我不会赘述。如果您很好奇,请访问https://en.wikibooks.org/wiki/Haskell/Understanding_monads。)

  1. 对于I / O Action monad, toMonad(aValue) =一个相关的值为aValue但其作用是什么都不做的动作。
  2. 对于列表monad, toMonad(aValue) =包含aValue作为其唯一条目的列表。 参见图9。
  3. 对于Maybe单toMonad(number)toMonad(number) =包含该number的Maybe值

    请记住,包含5的Maybe值与普通的旧数字5并不完全相同。

  4. 对于Writer monad, toMonad返回包含空字符串的monad。

    toMonad(number) = (number, "")

而已!

monad是具有bind函数和toMonad函数的类型。 bind功能从monad机械地移动到monad,而没有显式揭示monad的相关值。

从功能上考虑时,单子弹到处都是。 使用I / O操作monad,代码中的任何表达式都不代表用户的输入。 因此,系统状态不会在程序中的任何位置明确表示。 代码中的表达式与时间无关。 您永远不会问“在程序的这一点上x是什么意思?” 而是,您问“ x在该程序中一劳永逸是什么意思?”

记住底线...

我们生活的世界是有状态的,对此我们无能为力。 Monad不会从系统中消除状态。 但是monad消除了程序代码中对状态的提及。 那可能很有帮助。

翻译自: https://www.infoq.com/articles/Understanding-Monads-guide-for-perplexed/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

http指南单子版

http指南单子版_了解单子。 困惑的指南相关推荐

  1. python数据科学指南是什么_《Python数据科学指南》——导读

    前 言 如今,我们生活在一个万物互联的世界,每天都在产生海量数据,不可能依靠人力去分析产生的所有数据并做出决策.人类的决策越来越多地被计算机辅助决策所取代,这也得益于数据科学的发展.数据科学已经深入到 ...

  2. python入门指南阅读答案_【python】入门指南1

    基础的数据结构:int, float, string 注意:python入门系列的文章的示例均使用python3来完成. #!/bin/python a= 1b= 1.0c= 'string' pri ...

  3. 模仿Hibernate的逆向工程_java版_源码下载

    在这篇blog:"Hibernate逆向工程原理_java版本"中谈到了Hibernate逆向工程原理. 我喜欢理论和实践相结合....so,今天我试着模仿hibernate的逆向 ...

  4. 启动rocketmq_RocketMQ 部署启动指南-Docker 版

    最近学习使用 rocketmq,需要搭建 rocketmq 服务端,本文主要记录 rocketmq 搭建过程以及这个过程踩到的一些坑. 准备工作 在搭建之前,我们需要做一些准备工作,这里我们需要使用 ...

  5. 《UML用户指南第二版》再次温读笔记(一)(downmoon)

    前言:最近,花点时间重读(也不知道是第几遍了)<UML用户指南第二版>这本书,感觉虽然对WEB程序开发而言,UML的应用是一个极大的挑战,然而,其中蕴含的基本原理和指导性却是历久弥新,耐人 ...

  6. 公司网站Silverlight版^_^

    公司网站Silverlight版^_^ 网站地址:http://www.ichinagames.com/Silverlight/ 预览图: posted on 2010-01-16 13:47 now ...

  7. 23V3有这种C语言表达式吗,数据结构(C语言版第2版_李云清)习题答案2012-12.doc

    数据结构(C语言版第2版_李云清)习题答案2012-12.doc 第 1 章 绪论 1.1 什么是数据结构? [答]:数据结构是指按一定的逻辑结构组成的一批数据,使用某种存储结构将这批数据存储 于计算 ...

  8. H5活动产品设计指南基础版

    本文来自 网易云社区 . H5一般页面不会很多,看似简单,实际上会有很多细节需要注意,我自己在做过了几个H5之后,发现了一些常犯的问题,做了小结,希望给新开始做H5的产品相关的同学提供一些帮助. 首先 ...

  9. 计算机中丢失msc,mscvr120.dll32位/64位版_修复计算机中丢失msvcr120.dll

    mscvr120.dll32位/64位版_修复计算机中丢失msvcr120.dll mscvr120.dll是系统的非常重要的一个文件,相信很多的人都是遇到文件丢失的情况,这个时候就需要你在下载一个d ...

最新文章

  1. 三类常见软件质量(Quality Attribute)属性的通俗解释
  2. 【初探HTML本相】道之真谛不过自然,html标签脱俗还真
  3. 请不要轻易使用 is_numberic 加入存在E字母
  4. C++ 写时拷贝 2
  5. 《Objective-C入门经典》——2.1节Objective-C世界中的面向对象程序设计
  6. django解决使用DateTimeField添加、修改记录时不动态更新时间的问题
  7. 输入挂(bzoj 2901: 矩阵求和)
  8. 【最短路径】 SPFA算法优化
  9. wordpress不登陆后台禁用插件
  10. JAVA 如何将String进行大小写转换
  11. 海康nvr sdk java调用,海康SDK开发NVR拍照功能
  12. linux压力测试工具post,linux 下如何做压力测试 ab 压力测试 - 沃森博客
  13. 建立 rsyslog 日志服务器
  14. 2019华为软件精英挑战赛总结篇
  15. 移动端meta设置大全(持续收集中。。。。)
  16. 兑换记录html页面,兑换码记录.html
  17. 【企业为什么要进行数字化转型】之数字时代新模式
  18. JAVA 如何使用延迟
  19. 惊!搜狐邮箱乱添附件!!!!
  20. 数电课程设计,数字电子钟具有时、分、秒以及校时功能

热门文章

  1. python单引号打不出来_python里单引号怎么打
  2. Java char字符单引号
  3. linux php 编译安装_linux下编译安装配置php5.6.30过程
  4. 断了线的风筝,只能让它飞,放过它
  5. mac pro 升级ssd_您可以在Mac中升级硬盘驱动器或SSD吗?
  6. BMPFont使用教程--免费的位图字体制作工具
  7. Mysql面试大全,看完可以吊打面试官
  8. AutoML:人工智能领域-自动化技术之机器学习自动化技术的简介(预处理→设计算法→训练模型→优化参数)、常用的工具或框架之详细攻略
  9. 2021年熔化焊接与热切割模拟试题及熔化焊接与热切割模拟考试
  10. 毕设 房价预测分析与可视